WORKING
This commit is contained in:
BIN
tests/e2e/__pycache__/conftest.cpython-311-pytest-8.3.5.pyc
Normal file
BIN
tests/e2e/__pycache__/conftest.cpython-311-pytest-8.3.5.pyc
Normal file
Binary file not shown.
Binary file not shown.
111
tests/e2e/conftest.py
Normal file
111
tests/e2e/conftest.py
Normal 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
|
||||
25
tests/e2e/docker/Dockerfile
Normal file
25
tests/e2e/docker/Dockerfile
Normal 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"]
|
||||
18
tests/e2e/docker/run_blender.sh
Normal file
18
tests/e2e/docker/run_blender.sh
Normal 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
|
||||
5
tests/e2e/requirements.txt
Normal file
5
tests/e2e/requirements.txt
Normal 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
|
||||
Binary file not shown.
143
tests/e2e/scripts/test_texture_generation.py
Normal file
143
tests/e2e/scripts/test_texture_generation.py
Normal 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()
|
||||
150
tests/e2e/test_addon_real_blender.py
Normal file
150
tests/e2e/test_addon_real_blender.py
Normal 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}"
|
||||
)
|
||||
Reference in New Issue
Block a user