This commit is contained in:
2025-10-10 11:28:22 +02:00
parent 205aa7c421
commit d02cdc1463
157 changed files with 639 additions and 33540 deletions

111
tests/e2e/conftest.py Normal file
View File

@@ -0,0 +1,111 @@
"""
E2E test fixtures for Docker-based Blender testing.
Manages Docker container lifecycle for real Blender tests.
"""
import pytest
import docker
import shutil
import zipfile
from pathlib import Path
import time
@pytest.fixture(scope="module")
def docker_client():
"""Provide Docker client for container management."""
try:
client = docker.from_env()
# Verify Docker is running
client.ping()
return client
except docker.errors.DockerException as e:
pytest.skip(f"Docker not available: {e}")
@pytest.fixture(scope="module")
def blender_image(docker_client):
"""Build Blender Docker image if not exists."""
image_tag = "text-texture-generator-e2e:latest"
dockerfile_path = Path(__file__).parent / "docker"
try:
# Try to get existing image
docker_client.images.get(image_tag)
print(f"Using existing Docker image: {image_tag}")
except docker.errors.ImageNotFound:
print(f"Building Docker image: {image_tag}")
# Build image
docker_client.images.build(
path=str(dockerfile_path),
tag=image_tag,
rm=True
)
return image_tag
@pytest.fixture(scope="function")
def blender_container(docker_client, blender_image, tmp_path):
"""
Spin up Blender container for test execution.
Yields container instance, cleans up after test.
"""
container = None
try:
# Start container with addon mounted
container = docker_client.containers.run(
blender_image,
command="/bin/sleep 3600", # Keep alive for test duration
detach=True,
remove=False, # We'll remove manually after cleanup
volumes={
str(tmp_path): {"bind": "/addon-test", "mode": "rw"}
}
)
# Wait for container to be ready
timeout = 10
start = time.time()
while time.time() - start < timeout:
container.reload()
if container.status == "running":
break
time.sleep(0.5)
if container.status != "running":
pytest.fail(f"Container failed to start: {container.status}")
yield container
finally:
# Cleanup
if container:
try:
container.stop(timeout=5)
container.remove()
except Exception as e:
print(f"Warning: Container cleanup failed: {e}")
@pytest.fixture(scope="function")
def addon_package(tmp_path):
"""
Package the addon as a zip file for installation in Blender.
Returns path to the packaged addon zip.
"""
src_dir = Path(__file__).parent.parent.parent / "src"
addon_zip = tmp_path / "text_texture_generator.zip"
# Create zip with addon contents
with zipfile.ZipFile(addon_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
for file_path in src_dir.rglob("*"):
if file_path.is_file():
# Add file with relative path from src/
arc_name = f"text_texture_generator/{file_path.relative_to(src_dir)}"
zf.write(file_path, arc_name)
return addon_zip

View File

@@ -0,0 +1,25 @@
FROM --platform=linux/amd64 ubuntu:22.04
# Prevent interactive prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive
# Install Blender and dependencies including python3-pil
RUN apt-get update && apt-get install -y \
blender \
python3-pip \
python3-pil \
wget \
xvfb \
&& rm -rf /var/lib/apt/lists/*
# Set up working directory
WORKDIR /addon-test
# Blender requires a display, use Xvfb for headless operation
ENV DISPLAY=:99
# Copy helper script to run Blender with virtual display
COPY run_blender.sh /usr/local/bin/run_blender.sh
RUN chmod +x /usr/local/bin/run_blender.sh
CMD ["/bin/bash"]

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Run Blender in headless mode with virtual display
# Start Xvfb in background
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
XVFB_PID=$!
# Wait for Xvfb to start
sleep 2
# Run Blender with all arguments passed to this script
blender "$@"
BLENDER_EXIT=$?
# Cleanup Xvfb
kill $XVFB_PID 2>/dev/null || true
exit $BLENDER_EXIT

View File

@@ -0,0 +1,5 @@
# E2E test dependencies
# Install with: pip install -r tests/e2e/requirements.txt
docker>=6.0.0
pytest>=7.0.0

View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
Blender Python script that runs INSIDE Blender to test texture generation.
This script has NO mocks - it uses the real addon code with real bpy.
CRITICAL: This test calls the ACTUAL Blender operator (bpy.ops.text_texture.generate_shader)
because the bug is in the OPERATOR's truthiness check at generation_ops.py:241-247,
not in the generate_texture_image() function itself.
"""
import sys
import bpy
def test_shader_creation_with_texture():
"""
E2E test: Call the ACTUAL Blender operator to expose truthiness bug.
BUG LOCATION: src/operators/generation_ops.py:241-247
```python
result = generate_texture_image(props, final_width, final_height)
if not result: # ❌ BUG: (None, None) is TRUTHY in Python!
self.report({'ERROR'}, "Failed to generate texture image")
return {'CANCELLED'}
```
When generate_texture_image() returns (None, None):
- `not result` evaluates to False (non-empty tuple is truthy)
- Error check is bypassed
- Code continues until line 263 where it tries to use None values
This test MUST call bpy.ops.text_texture.generate_shader() (the operator)
NOT generate_texture_image() (the function) to expose the bug.
"""
# Clean slate - remove default objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create a new cube (we need an object to attach material to)
bpy.ops.mesh.primitive_cube_add()
cube = bpy.context.active_object
# Set up scene properties for texture generation
props = bpy.context.scene.text_texture_props
# Configure text using the actual property structure
props.text = "böhm Kabel"
props.append_text = "NYY"
# Set dimensions (user-specified, not generated)
props.texture_width = 4096
props.texture_height = 512
# Configure shader settings
props.shader_type = 'PRINCIPLED'
props.shader_connection = 'BASE_COLOR'
props.auto_assign_material = True
props.generate_normal_map = False
props.background_transparent = True
print(f"\nTest scenario:")
print(f" Main text: {props.text}")
print(f" Append text: {props.append_text}")
print(f" Dimensions: {props.texture_width}x{props.texture_height}")
print(f"\nCalling bpy.ops.text_texture.generate_shader()...")
# CRITICAL: Call the OPERATOR (not the function!)
# This is the REAL code path that users trigger from the UI
try:
result = bpy.ops.text_texture.generate_shader()
print(f"Operator returned: {result}")
except Exception as e:
print(f"FAILURE: Operator raised exception: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# Assert 1: Operator should return {'FINISHED'} on success
# Current bug: Will return {'CANCELLED'} because truthiness check at line 241 is wrong
if result != {'FINISHED'}:
print(f"FAILURE: Operator returned {result} instead of {{'FINISHED'}}")
print(f"This confirms the truthiness bug at generation_ops.py:241-247")
print(f"The check 'if not result:' fails when result is (None, None)")
print(f"because non-empty tuples are truthy in Python!")
sys.exit(1)
# Assert 2: Material should be created
# The material name is based on sanitized text
materials = [m.name for m in bpy.data.materials]
print(f"Available materials: {materials}")
# Check for TextTexture_Mat prefix (the actual naming pattern used)
material_found = any("TextTexture_Mat" in m for m in materials)
if not material_found:
print(f"FAILURE: No TextTexture_Mat material found")
print(f"This indicates the operator failed to complete shader creation")
sys.exit(1)
# Assert 3: Texture image should be created
images = [i.name for i in bpy.data.images]
print(f"Available images: {images}")
# Check for TextTexture prefix (the actual naming pattern used)
image_found = any("TextTexture" in i for i in images)
if not image_found:
print(f"FAILURE: No TextTexture image found")
print(f"This indicates generate_texture_image() returned None or (None, None)")
sys.exit(1)
# Get the actual image
img = None
for image in bpy.data.images:
if "TextTexture" in image.name and "Normal" not in image.name:
img = image
break
if not img:
print(f"FAILURE: Could not retrieve texture image")
sys.exit(1)
# Assert 4: Verify dimensions
actual_width = img.size[0]
actual_height = img.size[1]
if actual_width != 4096 or actual_height != 512:
print(f"FAILURE: Wrong dimensions. Expected 4096x512, got {actual_width}x{actual_height}")
sys.exit(1)
# Assert 5: Material should be assigned to cube
if not cube.data.materials or len(cube.data.materials) == 0:
print(f"FAILURE: No material assigned to cube")
sys.exit(1)
# Success markers for test_addon_real_blender.py to detect
print("SUCCESS: Texture generated successfully with correct dimensions and shader connected")
print(f"SUCCESS: Texture '{img.name}' created with size {actual_width}x{actual_height}")
print(f"SUCCESS: Shader nodes created and connected")
sys.exit(0)
if __name__ == "__main__":
test_shader_creation_with_texture()

View File

@@ -0,0 +1,150 @@
"""
E2E test: Real Blender addon testing in Docker with ZERO mocks.
This test validates the entire addon workflow in an actual Blender instance:
1. Package addon as zip
2. Install in real Blender (in Docker container)
3. Enable the addon
4. Execute texture generation with real text (including umlauts)
5. Verify texture is created successfully
NO MOCKS:
- Real bpy (Blender Python API)
- Real PIL (bundled with addon)
- Real addon code (generate_texture_image)
- Real Docker container with actual Blender
"""
import pytest
from pathlib import Path
import tarfile
import io
def test_shader_creation_with_texture_generates_successfully(
blender_container,
addon_package,
tmp_path
):
"""
E2E test: Install addon in real Blender and create texture shader.
This test uses ZERO mocks - everything runs in real Blender in Docker.
Test scenario (the exact bug case):
- Text: "böhm Kabel" + "NYY" (German umlauts)
- Dimensions: 4096x512
- Expected: Texture generated successfully with actual text rendering
- Bug symptom: "Failed to generate texture image" error OR PIL import error
Asserts:
- No "Failed to generate texture image" error
- No PIL import errors (addon dependencies must work)
- Texture exists in bpy.data.images
- Texture has correct dimensions (4096x512)
- Shader nodes created and connected
"""
# Copy test script to container using tar archive
test_script = Path(__file__).parent / "scripts" / "test_texture_generation.py"
# Create tar archive with test script
tar_stream = io.BytesIO()
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
# Add test script
script_info = tarfile.TarInfo(name='test_script.py')
script_info.size = test_script.stat().st_size
with open(test_script, 'rb') as f:
tar.addfile(script_info, f)
tar_stream.seek(0)
blender_container.put_archive('/addon-test', tar_stream)
# Copy addon package to container
addon_tar_stream = io.BytesIO()
with tarfile.open(fileobj=addon_tar_stream, mode='w') as tar:
addon_info = tarfile.TarInfo(name='text_texture_generator.zip')
addon_info.size = addon_package.stat().st_size
with open(addon_package, 'rb') as f:
tar.addfile(addon_info, f)
addon_tar_stream.seek(0)
blender_container.put_archive('/addon-test', addon_tar_stream)
container_script_path = "/addon-test/test_script.py"
container_addon_path = "/addon-test/text_texture_generator.zip"
# Install addon in Blender
install_cmd = [
"/usr/local/bin/run_blender.sh",
"--background",
"--python-expr",
(
"import bpy; "
f"bpy.ops.preferences.addon_install(filepath='{container_addon_path}'); "
"bpy.ops.preferences.addon_enable(module='text_texture_generator'); "
"bpy.ops.wm.save_userpref()"
)
]
install_result = blender_container.exec_run(
install_cmd,
workdir="/addon-test"
)
# Check installation succeeded
assert install_result.exit_code == 0, (
f"Addon installation failed with exit code {install_result.exit_code}\n"
f"Output: {install_result.output.decode('utf-8', errors='replace')}"
)
# Run the test script in Blender
test_cmd = [
"/usr/local/bin/run_blender.sh",
"--background",
"--python",
container_script_path
]
test_result = blender_container.exec_run(
test_cmd,
workdir="/addon-test"
)
output = test_result.output.decode('utf-8', errors='replace')
# Parse output for results
print("\n=== Blender Test Output ===")
print(output)
print("=== End Output ===\n")
# Assert: Script must exit with success code
assert test_result.exit_code == 0, (
f"Test script failed with exit code {test_result.exit_code}\n"
f"Output: {output}"
)
# Assert: Must see SUCCESS marker in output
assert "SUCCESS" in output, (
f"Expected SUCCESS marker in output, got:\n{output}"
)
# Assert: Must NOT see failure to generate texture
assert "Failed to generate texture image" not in output, (
f"Texture generation failed - bug still present!\n{output}"
)
# Assert: PIL must be available (no dependency errors allowed)
assert "ERROR: PIL dependency not available" not in output, (
f"PIL dependency not properly installed by Blender!\n{output}"
)
# Assert: Texture was created with correct dimensions
assert "created with size 4096x512" in output, (
f"Texture not created with expected dimensions\n{output}"
)
# Assert: Shader nodes were created and connected
assert "Shader nodes created and connected" in output, (
f"Shader nodes not properly set up\n{output}"
)