""" E2E regression test for PIL/Pillow dependency availability. This test reproduces the production error: ERROR: PIL dependency not available despite manifest declaration: No module named 'PIL' CRITICAL DISCOVERY: The Docker image pre-installs python3-pil (line 10 in Dockerfile), which creates a FALSE GREEN. Real users don't have PIL pre-installed. This test simulates the REAL production scenario: 1. Blender environment WITHOUT PIL pre-installed (like fresh user installation) 2. Addon declares PIL in manifest 3. Addon tries to import PIL at runtime 4. Import FAILS because manifest declaration doesn't guarantee installation This is a FAILING test that must pass after GREEN implements proper PIL installation. NO MOCKS - runs in real Docker+Blender environment, testing actual failure mode. """ import pytest from pathlib import Path import tarfile import io def test_pil_dependency_available_in_blender_without_system_package( blender_container, addon_package, tmp_path ): """ Regression test for: ERROR: PIL dependency not available despite manifest declaration This test verifies that PIL/Pillow CAN be imported within Blender's Python context after the addon is installed, EVEN WITHOUT system PIL pre-installed. Test strategy: 1. Use real Docker+Blender environment (via existing E2E fixtures) 2. REMOVE system-installed PIL to simulate fresh Blender installation 3. Install the addon (which declares PIL as dependency) 4. Attempt to import PIL 5. Assert PIL IS importable (test MUST FAIL initially, proving the bug) Current behavior (BUG): - PIL import fails with ModuleNotFoundError - Manifest declaration doesn't guarantee runtime availability Expected behavior (after GREEN fix): - PIL imports successfully via addon's wheels mechanism - Addon can use PIL for text rendering """ # First, remove system-installed PIL to simulate fresh Blender remove_pil_cmd = [ "apt-get", "remove", "-y", "python3-pil" ] remove_result = blender_container.exec_run( remove_pil_cmd, user="root" # Need root to remove packages ) print("\n=== Removing system PIL ===") print(remove_result.output.decode('utf-8', errors='replace')) print("=== PIL Removal Complete ===\n") # Now test if addon can provide PIL through its dependency mechanism script_content = """ import sys # Test PIL import after addon installation WITHOUT system PIL # The addon's dependency mechanism should make PIL available try: # Attempt the exact imports used by the addon from PIL import Image, ImageDraw, ImageFont # Verify PIL is fully functional img = Image.new('RGB', (100, 100), color='white') draw = ImageDraw.Draw(img) import PIL pil_version = PIL.__version__ print(f"SUCCESS: PIL imported successfully (version {pil_version})") print("Image.new() works: True") print("ImageDraw.Draw() works: True") print("PIL is available through addon's dependency mechanism") sys.exit(0) except ModuleNotFoundError as e: print(f"FAILURE: ModuleNotFoundError: {e}") print("") print("This is the BUG - manifest declaration doesn't provide PIL") sys.exit(1) except ImportError as e: print(f"FAILURE: ImportError: {e}") sys.exit(1) """ # Copy test script to container tar_stream = io.BytesIO() with tarfile.open(fileobj=tar_stream, mode='w') as tar: script_info = tarfile.TarInfo(name='test_pil_import.py') script_bytes = script_content.encode('utf-8') script_info.size = len(script_bytes) tar.addfile(script_info, io.BytesIO(script_bytes)) tar_stream.seek(0) blender_container.put_archive('/addon-test', tar_stream) # Copy addon package 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_pil_import.py" container_addon_path = "/addon-test/text_texture_generator.zip" # Install addon 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" ) 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 PIL import test 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') print("\n=== PIL Import Test Output ===") print(output) print("=== End Output ===\n") # THIS IS THE KEY ASSERTION FOR RED PHASE # We assert PIL SHOULD be available (exit code 0) # This MUST FAIL initially, proving the bug exists assert test_result.exit_code == 0, ( f"PIL import FAILED in Blender Python context.\n" f"Exit code: {test_result.exit_code}\n" f"Output:\n{output}\n\n" f"BUG CONFIRMED: Addon's manifest declares PIL but it's not available at runtime.\n" f"The addon cannot render text without PIL.\n" f"GREEN phase must implement proper PIL installation mechanism." ) # Verify PIL actually works (full functionality check) assert "SUCCESS: PIL imported successfully" in output, ( "PIL import did not produce success message" ) assert "Image.new() works: True" in output, ( "PIL Image.new() failed - PIL not fully functional" ) assert "ImageDraw.Draw() works: True" in output, ( "PIL ImageDraw.Draw() failed - PIL not fully functional" ) def test_addon_text_rendering_works_without_system_pil( blender_container, addon_package, tmp_path ): """ Extended test: Verify addon's text rendering works without system PIL. This tests the actual production use case - generating a texture with text. """ # Remove system PIL remove_pil_cmd = [ "apt-get", "remove", "-y", "python3-pil" ] blender_container.exec_run(remove_pil_cmd, user="root") script_content = """ import sys import bpy # Enable addon bpy.ops.preferences.addon_enable(module='text_texture_generator') # Test actual text texture generation (the real use case) try: from text_texture_generator.core.generation_engine import generate_texture_image from text_texture_generator.properties import TEXT_TEXTURE_Properties # Create properties mock class MockProps: text = "Test Text" prepend_text = "" append_text = "" prepend_append_layout = 'HORIZONTAL' prepend_margin = 10 append_margin = 10 background_transparent = False background_color = (0.2, 0.2, 0.2, 1.0) text_color = (1.0, 1.0, 1.0, 1.0) font_size = 48 font_path = "default" use_custom_font = False custom_font_path = "" enable_text_fitting = False props = MockProps() # Try to generate texture (this will fail if PIL isn't available) result = generate_texture_image(props, 512, 512) if result and result[0]: print("SUCCESS: Text texture generated successfully") print("Addon text rendering works without system PIL") sys.exit(0) else: print("FAILURE: Texture generation returned None") sys.exit(1) except Exception as e: print(f"FAILURE: {e}") import traceback traceback.print_exc() sys.exit(1) """ # Copy test script tar_stream = io.BytesIO() with tarfile.open(fileobj=tar_stream, mode='w') as tar: script_info = tarfile.TarInfo(name='test_rendering.py') script_bytes = script_content.encode('utf-8') script_info.size = len(script_bytes) tar.addfile(script_info, io.BytesIO(script_bytes)) tar_stream.seek(0) blender_container.put_archive('/addon-test', tar_stream) # Copy addon package 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_rendering.py" container_addon_path = "/addon-test/text_texture_generator.zip" # Install addon 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()" ) ] blender_container.exec_run(install_cmd, workdir="/addon-test") # Run rendering test 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') print("\n=== Text Rendering Test Output ===") print(output) print("=== End Output ===\n") # Assert rendering works (MUST FAIL initially) assert test_result.exit_code == 0, ( f"Text rendering failed without system PIL\n" f"Exit code: {test_result.exit_code}\n{output}\n" f"BUG: Addon cannot render text without PIL dependency" ) assert "SUCCESS: Text texture generated successfully" in output