remove multiline
This commit is contained in:
@@ -31,12 +31,13 @@ Comprehensive test suite for the Text Texture Generator Blender addon, focusing
|
||||
- Text that fits within constraints
|
||||
- Text requiring wrapping
|
||||
- Single word too long edge case
|
||||
- **TestProcessMultilineText** (4 tests)
|
||||
- **TestProcessTextContent** (5 tests)
|
||||
|
||||
- Single line processing
|
||||
- Multiline text handling
|
||||
- Height limit constraints
|
||||
- Empty text edge cases
|
||||
- Newline normalization
|
||||
- Multiple newline collapsing
|
||||
- Mixed whitespace handling
|
||||
- Empty string handling
|
||||
- No-newline passthrough
|
||||
|
||||
- **TestCalculateTextMetrics** (2 tests)
|
||||
|
||||
@@ -138,7 +139,7 @@ Comprehensive test suite for the Text Texture Generator Blender addon, focusing
|
||||
|
||||
### Core Algorithms
|
||||
|
||||
✅ Text wrapping and multiline processing
|
||||
✅ Text wrapping and newline normalization
|
||||
✅ Largest rectangle in histogram algorithm
|
||||
✅ Text positioning and alignment
|
||||
✅ Optimal font size calculation
|
||||
|
||||
@@ -144,7 +144,7 @@ def sample_text_long():
|
||||
|
||||
@pytest.fixture
|
||||
def sample_text_multiline():
|
||||
"""Multiline sample text for testing"""
|
||||
"""Sample text containing newlines for testing"""
|
||||
return "Line 1\nLine 2\nLine 3\nLine 4"
|
||||
|
||||
|
||||
@@ -259,4 +259,4 @@ def mock_props():
|
||||
props.normal_map_strength = 1.0
|
||||
props.normal_map_blur_radius = 0
|
||||
props.normal_map_invert = False
|
||||
return props
|
||||
return props
|
||||
|
||||
@@ -114,13 +114,6 @@ def addon_package(tmp_path, request):
|
||||
content = content.replace('VERSION_TYPE = "free"', f'VERSION_TYPE = "{version_type}"')
|
||||
constants_file.write_text(content)
|
||||
|
||||
# For FREE version, exclude PRO-only files
|
||||
if version_type == "free":
|
||||
# Remove text_editor_ops.py (multiline editor is PRO-only)
|
||||
text_editor_ops = temp_src / "operators" / "text_editor_ops.py"
|
||||
if text_editor_ops.exists():
|
||||
text_editor_ops.unlink()
|
||||
|
||||
# Create zip from patched source
|
||||
addon_zip = tmp_path / "text_texture_generator.zip"
|
||||
with zipfile.ZipFile(addon_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
@@ -129,4 +122,4 @@ def addon_package(tmp_path, request):
|
||||
arc_name = f"text_texture_generator/{file_path.relative_to(temp_src)}"
|
||||
zf.write(file_path, arc_name)
|
||||
|
||||
return addon_zip
|
||||
return addon_zip
|
||||
|
||||
@@ -800,56 +800,33 @@ print("SUCCESS: Single-line text remains unchanged")
|
||||
assert test_result.exit_code == 0, f"Single-line test failed:\n{output}"
|
||||
|
||||
|
||||
def test_free_version_multiline_editor_operator_poll_blocks_access(blender_container, addon_package):
|
||||
def test_free_version_no_multiline_artifacts(blender_container, addon_package):
|
||||
"""
|
||||
SPECIFICATION: FREE version must block multiline editor operator via poll().
|
||||
SPECIFICATION: Multiline feature must be absent from the addon.
|
||||
|
||||
Expected behavior (VERSION_TYPE="free"):
|
||||
- TEXT_TEXTURE_OT_edit_multiline_text.poll() returns False
|
||||
- Operator button should be disabled/grayed out in UI
|
||||
- has_multiline_text() returns False
|
||||
|
||||
Current state (VERSION_TYPE="full"):
|
||||
- Test SKIPS (multiline editor is available)
|
||||
This verification runs inside Blender to ensure that stale helpers or
|
||||
operators are not exposed through the package API.
|
||||
"""
|
||||
version_type, enabled_features = get_version_type(blender_container, addon_package)
|
||||
|
||||
if version_type == "full":
|
||||
pytest.skip("Test requires VERSION_TYPE='free' - currently 'full' (specification test)")
|
||||
|
||||
script_content = """
|
||||
import bpy
|
||||
import sys
|
||||
|
||||
# Verify multiline text feature is disabled
|
||||
from text_texture_generator.utils.constants import has_multiline_text
|
||||
|
||||
if has_multiline_text():
|
||||
print(f"FAIL: has_multiline_text() should return False in FREE version")
|
||||
def fail(msg):
|
||||
print(f"FAIL: {msg}")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify operator is a MockOperator (text_editor_ops.py should be excluded)
|
||||
# has_multiline_text helper must be gone
|
||||
try:
|
||||
import text_texture_generator.operators.text_editor_ops as text_editor_ops
|
||||
print(f"FAIL: text_editor_ops module should not exist in FREE version")
|
||||
sys.exit(1)
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass # Expected - module excluded from FREE build
|
||||
from text_texture_generator.utils.constants import has_multiline_text
|
||||
fail("has_multiline_text() helper is still importable")
|
||||
except ImportError:
|
||||
print("SUCCESS: has_multiline_text helper removed")
|
||||
|
||||
# Import operator from main module (should be MockOperator)
|
||||
import text_texture_generator
|
||||
op_class = text_texture_generator.TEXT_TEXTURE_OT_edit_multiline_text
|
||||
|
||||
# Verify it's a MockOperator (no bl_rna, empty bl_idname)
|
||||
if hasattr(op_class, 'bl_rna'):
|
||||
print(f"FAIL: Operator should be MockOperator (has bl_rna)")
|
||||
sys.exit(1)
|
||||
|
||||
if not hasattr(op_class, 'bl_idname') or op_class.bl_idname != "":
|
||||
print(f"FAIL: MockOperator should have empty bl_idname, got: {getattr(op_class, 'bl_idname', 'MISSING')}")
|
||||
sys.exit(1)
|
||||
|
||||
print("SUCCESS: Multiline editor operator correctly blocked via poll()")
|
||||
# Legacy multiline operator must also be absent
|
||||
try:
|
||||
from text_texture_generator import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
fail("TEXT_TEXTURE_OT_edit_multiline_text is still exposed")
|
||||
except ImportError:
|
||||
print("SUCCESS: Multiline editor operator removed")
|
||||
"""
|
||||
|
||||
tar_stream = io.BytesIO()
|
||||
@@ -873,61 +850,6 @@ print("SUCCESS: Multiline editor operator correctly blocked via poll()")
|
||||
output = test_result.output.decode('utf-8', errors='replace')
|
||||
|
||||
assert test_result.exit_code == 0, f"Operator poll test failed:\n{output}"
|
||||
assert "SUCCESS: Multiline editor operator correctly blocked" in output
|
||||
assert "SUCCESS: has_multiline_text helper removed" in output
|
||||
assert "SUCCESS: Multiline editor operator removed" in output
|
||||
|
||||
|
||||
def test_free_version_multiline_editor_invoke_fails_gracefully(blender_container, addon_package):
|
||||
"""
|
||||
SPECIFICATION: FREE version must fail gracefully if operator is invoked.
|
||||
|
||||
Expected behavior (VERSION_TYPE="free"):
|
||||
- Even if poll() is bypassed, invoke should fail gracefully
|
||||
- No crashes or exceptions
|
||||
- Returns appropriate error status
|
||||
|
||||
Current state (VERSION_TYPE="full"):
|
||||
- Test SKIPS (operator works normally)
|
||||
"""
|
||||
version_type, enabled_features = get_version_type(blender_container, addon_package)
|
||||
|
||||
if version_type == "full":
|
||||
pytest.skip("Test requires VERSION_TYPE='free' - currently 'full' (specification test)")
|
||||
|
||||
script_content = """
|
||||
import bpy
|
||||
import sys
|
||||
|
||||
# Attempt to import the operator - in FREE version, this module should not exist
|
||||
try:
|
||||
from text_texture_generator.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
# If we get here in FREE version, something is wrong
|
||||
print("ERROR: text_editor_ops should not exist in FREE version")
|
||||
sys.exit(1)
|
||||
except ImportError:
|
||||
# Expected in FREE version - module should not exist
|
||||
print("SUCCESS: Multiline editor module correctly absent in FREE version")
|
||||
sys.exit(0)
|
||||
"""
|
||||
|
||||
tar_stream = io.BytesIO()
|
||||
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
|
||||
script_info = tarfile.TarInfo(name='test_editor_invoke.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)
|
||||
|
||||
test_cmd = [
|
||||
"/usr/local/bin/run_blender.sh",
|
||||
"--background",
|
||||
"--python",
|
||||
"/addon-test/test_editor_invoke.py"
|
||||
]
|
||||
|
||||
test_result = blender_container.exec_run(test_cmd, workdir="/addon-test")
|
||||
output = test_result.output.decode('utf-8', errors='replace')
|
||||
|
||||
assert test_result.exit_code == 0, f"Operator invoke test failed:\n{output}"
|
||||
assert "SUCCESS: Multiline editor module correctly absent in FREE version" in output
|
||||
@@ -516,286 +516,13 @@ except Exception as e:
|
||||
assert "Can set 4096px: True" in output
|
||||
|
||||
|
||||
def test_pro_version_multiline_editor_operator_poll_allows_access(
|
||||
def test_pro_version_text_normalization_matches_free_version(
|
||||
blender_container,
|
||||
addon_package,
|
||||
tmp_path
|
||||
):
|
||||
"""
|
||||
Verify multiline editor operator poll() returns True in PRO version.
|
||||
|
||||
Expected behavior (VERSION_TYPE="full"):
|
||||
- TEXT_TEXTURE_OT_edit_multiline_text.poll() returns True
|
||||
- Operator button should be enabled in UI
|
||||
- has_multiline_text() returns True
|
||||
"""
|
||||
script_content = """
|
||||
import bpy
|
||||
import sys
|
||||
|
||||
# Enable addon
|
||||
try:
|
||||
bpy.ops.preferences.addon_enable(module='text_texture_generator')
|
||||
except Exception as e:
|
||||
print(f"ERROR enabling addon: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify multiline text feature is enabled
|
||||
try:
|
||||
from text_texture_generator.utils.constants import has_multiline_text, VERSION_TYPE
|
||||
|
||||
print(f"VERSION_TYPE: {VERSION_TYPE}")
|
||||
|
||||
if not has_multiline_text():
|
||||
print(f"FAIL: has_multiline_text() should return True in PRO version")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify operator poll returns True
|
||||
from text_texture_generator.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
|
||||
# Create a minimal mock context
|
||||
class MockContext:
|
||||
pass
|
||||
|
||||
context = MockContext()
|
||||
poll_result = TEXT_TEXTURE_OT_edit_multiline_text.poll(context)
|
||||
|
||||
if not poll_result:
|
||||
print(f"FAIL: Operator poll() should return True in PRO version")
|
||||
sys.exit(1)
|
||||
|
||||
print("SUCCESS: Multiline editor operator poll() returns True")
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {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_editor_poll.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_editor_poll.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
|
||||
|
||||
# Run poll verification 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=== Multiline Editor Poll Output ===")
|
||||
print(output)
|
||||
print("=== End Output ===\n")
|
||||
|
||||
assert test_result.exit_code == 0, (
|
||||
f"Poll verification failed with exit code {test_result.exit_code}\n{output}"
|
||||
)
|
||||
|
||||
assert "VERSION_TYPE: full" in output
|
||||
assert "SUCCESS: Multiline editor operator poll() returns True" in output
|
||||
|
||||
|
||||
def test_pro_version_multiline_editor_can_be_invoked(
|
||||
blender_container,
|
||||
addon_package,
|
||||
tmp_path
|
||||
):
|
||||
"""
|
||||
Verify multiline editor operator can be successfully invoked in PRO version.
|
||||
|
||||
Expected behavior (VERSION_TYPE="full"):
|
||||
- Operator can be invoked without errors
|
||||
- Dialog is opened (invoke_props_dialog called)
|
||||
- temp_text is initialized from current text property
|
||||
"""
|
||||
script_content = """
|
||||
import bpy
|
||||
import sys
|
||||
|
||||
# Enable addon
|
||||
try:
|
||||
bpy.ops.preferences.addon_enable(module='text_texture_generator')
|
||||
except Exception as e:
|
||||
print(f"ERROR enabling addon: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test operator invocation
|
||||
try:
|
||||
from text_texture_generator.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
|
||||
# Create scene and properties
|
||||
bpy.ops.mesh.primitive_cube_add()
|
||||
context = bpy.context
|
||||
|
||||
# Verify operator is registered and accessible
|
||||
# We can't directly instantiate Blender operators, but we can verify registration
|
||||
if not hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'bl_idname'):
|
||||
print("ERROR: Operator missing bl_idname")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify it's a real Blender operator (has bl_rna)
|
||||
if not hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'bl_rna'):
|
||||
print("ERROR: Operator is not a proper Blender operator (missing bl_rna)")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify temp_text property is defined (check annotations or __annotations__)
|
||||
has_temp_text = (
|
||||
hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'temp_text') or
|
||||
(hasattr(TEXT_TEXTURE_OT_edit_multiline_text, '__annotations__') and
|
||||
'temp_text' in TEXT_TEXTURE_OT_edit_multiline_text.__annotations__)
|
||||
)
|
||||
|
||||
if not has_temp_text:
|
||||
print("ERROR: Operator missing temp_text property")
|
||||
print(f"Available attributes: {dir(TEXT_TEXTURE_OT_edit_multiline_text)}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Operator registered successfully")
|
||||
print("temp_text property exists: True")
|
||||
print("SUCCESS: Multiline editor operator can be invoked")
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {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_editor_invoke.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_editor_invoke.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
|
||||
|
||||
# Run invoke 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=== Multiline Editor Invoke Output ===")
|
||||
print(output)
|
||||
print("=== End Output ===\n")
|
||||
|
||||
assert test_result.exit_code == 0, (
|
||||
f"Invoke test failed with exit code {test_result.exit_code}\n{output}"
|
||||
)
|
||||
|
||||
assert "Operator registered successfully" in output
|
||||
assert "temp_text property exists: True" in output
|
||||
assert "SUCCESS: Multiline editor operator can be invoked" in output
|
||||
|
||||
|
||||
def test_pro_version_multiline_text_preserved_during_processing(
|
||||
blender_container,
|
||||
addon_package,
|
||||
tmp_path
|
||||
):
|
||||
"""
|
||||
Verify multiline text with newlines is preserved in PRO version.
|
||||
|
||||
Expected behavior (VERSION_TYPE="full"):
|
||||
- Text processor preserves \\n characters in PRO version
|
||||
- "Line 1\\nLine 2\\nLine 3" remains unchanged
|
||||
- No newline stripping in PRO mode
|
||||
Verify newline handling matches the single-line specification in PRO builds.
|
||||
"""
|
||||
script_content = """
|
||||
import bpy
|
||||
@@ -822,18 +549,15 @@ try:
|
||||
print(f"Input: {repr(input_text)}")
|
||||
print(f"Output: {repr(result)}")
|
||||
|
||||
# In PRO version, newlines should be preserved
|
||||
if VERSION_TYPE == "full" or VERSION_TYPE == "pro":
|
||||
if "\\n" not in result:
|
||||
print(f"FAIL: Newlines should be preserved in PRO version")
|
||||
sys.exit(1)
|
||||
|
||||
newline_count = result.count("\\n")
|
||||
if newline_count != 2:
|
||||
print(f"FAIL: Expected 2 newlines, got {newline_count}")
|
||||
sys.exit(1)
|
||||
if "\\n" in result:
|
||||
print("FAIL: Result should not contain newline characters")
|
||||
sys.exit(1)
|
||||
|
||||
print("SUCCESS: Multiline text preserved in PRO version")
|
||||
if result != "Line 1 Line 2 Line 3":
|
||||
print(f"FAIL: Expected normalized output, got {repr(result)}")
|
||||
sys.exit(1)
|
||||
|
||||
print("SUCCESS: Text content normalized in PRO version")
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
@@ -845,7 +569,7 @@ except Exception as e:
|
||||
# Copy test script
|
||||
tar_stream = io.BytesIO()
|
||||
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
|
||||
script_info = tarfile.TarInfo(name='test_multiline_preserve.py')
|
||||
script_info = tarfile.TarInfo(name='test_text_normalization.py')
|
||||
script_bytes = script_content.encode('utf-8')
|
||||
script_info.size = len(script_bytes)
|
||||
tar.addfile(script_info, io.BytesIO(script_bytes))
|
||||
@@ -864,7 +588,7 @@ except Exception as e:
|
||||
addon_tar_stream.seek(0)
|
||||
blender_container.put_archive('/addon-test', addon_tar_stream)
|
||||
|
||||
container_script_path = "/addon-test/test_multiline_preserve.py"
|
||||
container_script_path = "/addon-test/test_text_normalization.py"
|
||||
container_addon_path = "/addon-test/text_texture_generator.zip"
|
||||
|
||||
# Install addon
|
||||
@@ -887,7 +611,7 @@ except Exception as e:
|
||||
|
||||
assert install_result.exit_code == 0
|
||||
|
||||
# Run multiline preservation test
|
||||
# Run text normalization test
|
||||
test_cmd = [
|
||||
"/usr/local/bin/run_blender.sh",
|
||||
"--background",
|
||||
@@ -902,13 +626,13 @@ except Exception as e:
|
||||
|
||||
output = test_result.output.decode('utf-8', errors='replace')
|
||||
|
||||
print("\n=== Multiline Preservation Output ===")
|
||||
print("\n=== Text Normalization Output ===")
|
||||
print(output)
|
||||
print("=== End Output ===\n")
|
||||
|
||||
assert test_result.exit_code == 0, (
|
||||
f"Multiline preservation test failed with exit code {test_result.exit_code}\n{output}"
|
||||
f"Text normalization test failed with exit code {test_result.exit_code}\n{output}"
|
||||
)
|
||||
|
||||
assert "VERSION_TYPE: full" in output
|
||||
assert "SUCCESS: Multiline text preserved in PRO version" in output
|
||||
assert "SUCCESS: Text content normalized in PRO version" in output
|
||||
|
||||
@@ -4,7 +4,7 @@ Tests how the core modules work together.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -18,17 +18,13 @@ class TestTextGenerationPipeline:
|
||||
"""Integration tests for the complete text generation workflow"""
|
||||
|
||||
@pytest.mark.integration
|
||||
@patch('src.core.text_processor.process_multiline_text')
|
||||
@patch('src.core.text_processor.wrap_text_to_width')
|
||||
@patch('src.core.text_fitting.find_optimal_font_size')
|
||||
@patch('src.core.text_fitting.calculate_text_position')
|
||||
def test_basic_text_generation_workflow(self, mock_calc_position, mock_find_size, mock_process_text):
|
||||
def test_basic_text_generation_workflow(self, mock_calc_position, mock_find_size, mock_wrap_text):
|
||||
"""Test the basic workflow from text input to positioned output"""
|
||||
# Setup mocks
|
||||
mock_process_text.return_value = {
|
||||
'lines': ['Hello', 'World'],
|
||||
'total_height': 40,
|
||||
'line_count': 2
|
||||
}
|
||||
mock_wrap_text.return_value = ['Hello', 'World']
|
||||
mock_find_size.return_value = 24
|
||||
mock_calc_position.return_value = {
|
||||
'x': 50, 'y': 50,
|
||||
@@ -40,9 +36,10 @@ class TestTextGenerationPipeline:
|
||||
font = Mock()
|
||||
canvas_width, canvas_height = 300, 200
|
||||
|
||||
# Step 1: Process multiline text
|
||||
from src.core.text_processor import process_multiline_text
|
||||
processed = process_multiline_text(text, font, canvas_width, canvas_height)
|
||||
# Step 1: Normalize and wrap text
|
||||
from src.core.text_processor import process_text_content, wrap_text_to_width
|
||||
normalized = process_text_content(text)
|
||||
wrapped_lines = wrap_text_to_width(normalized, font, canvas_width)
|
||||
|
||||
# Step 2: Find optimal font size
|
||||
from src.core.text_fitting import find_optimal_font_size
|
||||
@@ -54,12 +51,12 @@ class TestTextGenerationPipeline:
|
||||
position = calculate_text_position(text, font, available_area, 'center')
|
||||
|
||||
# Verify workflow completed
|
||||
assert processed['line_count'] == 2
|
||||
assert wrapped_lines == ['Hello', 'World']
|
||||
assert optimal_size == 24
|
||||
assert position['x'] == 50 and position['y'] == 50
|
||||
|
||||
# Verify functions were called in sequence
|
||||
mock_process_text.assert_called_once()
|
||||
mock_wrap_text.assert_called_once_with(normalized, font, canvas_width)
|
||||
mock_find_size.assert_called_once()
|
||||
mock_calc_position.assert_called_once()
|
||||
|
||||
@@ -190,15 +187,13 @@ class TestErrorHandlingIntegration:
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_error_handling_in_text_processing_chain(self):
|
||||
"""Test error handling when text processing components fail"""
|
||||
with patch('src.core.text_processor.wrap_text_to_width') as mock_wrap:
|
||||
mock_wrap.side_effect = Exception("Text wrapping failed")
|
||||
"""Text wrapping should fail gracefully when PIL raises errors."""
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.side_effect = Exception("Text measurement failed")
|
||||
|
||||
# Test that multiline processing handles wrap failures
|
||||
from src.core.text_processor import process_multiline_text
|
||||
result = process_multiline_text("Test text", Mock(), 200, 100)
|
||||
from src.core.text_processor import wrap_text_to_width
|
||||
result = wrap_text_to_width("Test text", Mock(), 200)
|
||||
|
||||
# Should return fallback result instead of crashing
|
||||
assert 'lines' in result
|
||||
assert 'total_height' in result
|
||||
assert 'line_count' in result
|
||||
assert result == ["Test text"]
|
||||
|
||||
@@ -1,180 +1,47 @@
|
||||
"""
|
||||
Unit tests for version-aware text processing behavior.
|
||||
Tests that FREE version strips newlines while PRO version preserves them.
|
||||
Unit tests for text content normalization.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
class TestProcessTextContent:
|
||||
"""Verify newline and whitespace handling for text processing."""
|
||||
|
||||
class TestVersionAwareNewlineHandling:
|
||||
"""Test that text processing behavior differs between FREE and PRO versions"""
|
||||
|
||||
@patch('src.core.text_processor.VERSION_TYPE', 'free')
|
||||
def test_process_text_strips_newlines_in_free_version(self):
|
||||
"""FREE version should strip newlines and replace with spaces"""
|
||||
def test_collapses_single_newlines(self):
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\nLine 2\nLine 3"
|
||||
|
||||
text = "Line 1\nLine 2"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Line 1 Line 2 Line 3", \
|
||||
f"FREE version must strip newlines, got: {repr(result)}"
|
||||
|
||||
@patch('src.core.text_processor.VERSION_TYPE', 'full')
|
||||
def test_process_text_preserves_newlines_in_pro_version(self):
|
||||
"""PRO version should preserve newlines for multiline rendering"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\nLine 2\nLine 3"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Line 1\nLine 2\nLine 3", \
|
||||
f"PRO version must preserve newlines, got: {repr(result)}"
|
||||
assert result == "Line 1 Line 2"
|
||||
|
||||
@patch('src.core.text_processor.VERSION_TYPE', 'free')
|
||||
def test_process_text_strips_multiple_consecutive_newlines_in_free(self):
|
||||
"""FREE version should collapse multiple newlines to single space"""
|
||||
def test_collapses_multiple_newlines(self):
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
|
||||
text = "Line 1\n\n\nLine 2"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Line 1 Line 2", \
|
||||
f"FREE version must collapse multiple newlines, got: {repr(result)}"
|
||||
|
||||
@patch('src.core.text_processor.VERSION_TYPE', 'free')
|
||||
def test_process_text_handles_mixed_whitespace_in_free(self):
|
||||
"""FREE version should normalize mixed whitespace with newlines"""
|
||||
assert result == "Line 1 Line 2"
|
||||
|
||||
def test_normalizes_mixed_whitespace(self):
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\n \nLine 2"
|
||||
|
||||
text = "Line 1\n \t\nLine 2"
|
||||
result = process_text_content(text)
|
||||
|
||||
# Should have no newlines and normalized spacing
|
||||
assert "\n" not in result, \
|
||||
f"FREE version must remove all newlines, got: {repr(result)}"
|
||||
assert "Line 1" in result and "Line 2" in result, \
|
||||
f"Must preserve actual text content, got: {repr(result)}"
|
||||
|
||||
@patch('src.core.text_processor.VERSION_TYPE', 'full')
|
||||
def test_process_text_preserves_multiple_newlines_in_pro(self):
|
||||
"""PRO version should preserve multiple consecutive newlines"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\n\n\nLine 2"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Line 1\n\n\nLine 2", \
|
||||
f"PRO version must preserve all newlines, got: {repr(result)}"
|
||||
assert "\n" not in result
|
||||
assert result == "Line 1 Line 2"
|
||||
|
||||
@patch('src.core.text_processor.VERSION_TYPE', 'free')
|
||||
def test_process_empty_text_in_free(self):
|
||||
"""FREE version should handle empty text gracefully"""
|
||||
def test_handles_empty_string(self):
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
|
||||
result = process_text_content("")
|
||||
|
||||
assert result == "", \
|
||||
f"Empty text should remain empty, got: {repr(result)}"
|
||||
|
||||
@patch('src.core.text_processor.VERSION_TYPE', 'free')
|
||||
def test_process_text_without_newlines_in_free(self):
|
||||
"""FREE version should pass through text without newlines unchanged"""
|
||||
assert result == ""
|
||||
|
||||
def test_preserves_text_without_newlines(self):
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
|
||||
text = "Single line text"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Single line text", \
|
||||
f"Text without newlines should be unchanged, got: {repr(result)}"
|
||||
"""
|
||||
Unit tests for version-aware text processing behavior.
|
||||
Tests that FREE version strips newlines while PRO version preserves them.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestVersionAwareNewlineHandling:
|
||||
"""Test that text processing behavior differs between FREE and PRO versions"""
|
||||
|
||||
@patch('src.utils.constants.VERSION_TYPE', 'free')
|
||||
def test_process_text_strips_newlines_in_free_version(self):
|
||||
"""FREE version should strip newlines and replace with spaces"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\nLine 2\nLine 3"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Line 1 Line 2 Line 3", \
|
||||
f"FREE version must strip newlines, got: {repr(result)}"
|
||||
|
||||
@patch('src.utils.constants.VERSION_TYPE', 'full')
|
||||
def test_process_text_preserves_newlines_in_pro_version(self):
|
||||
"""PRO version should preserve newlines for multiline rendering"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\nLine 2\nLine 3"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Line 1\nLine 2\nLine 3", \
|
||||
f"PRO version must preserve newlines, got: {repr(result)}"
|
||||
|
||||
@patch('src.utils.constants.VERSION_TYPE', 'free')
|
||||
def test_process_text_strips_multiple_consecutive_newlines_in_free(self):
|
||||
"""FREE version should collapse multiple newlines to single space"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\n\n\nLine 2"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Line 1 Line 2", \
|
||||
f"FREE version must collapse multiple newlines, got: {repr(result)}"
|
||||
|
||||
@patch('src.utils.constants.VERSION_TYPE', 'free')
|
||||
def test_process_text_handles_mixed_whitespace_in_free(self):
|
||||
"""FREE version should normalize mixed whitespace with newlines"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\n \nLine 2"
|
||||
result = process_text_content(text)
|
||||
|
||||
# Should have no newlines and normalized spacing
|
||||
assert "\n" not in result, \
|
||||
f"FREE version must remove all newlines, got: {repr(result)}"
|
||||
assert "Line 1" in result and "Line 2" in result, \
|
||||
f"Must preserve actual text content, got: {repr(result)}"
|
||||
|
||||
@patch('src.utils.constants.VERSION_TYPE', 'full')
|
||||
def test_process_text_preserves_multiple_newlines_in_pro(self):
|
||||
"""PRO version should preserve multiple consecutive newlines"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Line 1\n\n\nLine 2"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Line 1\n\n\nLine 2", \
|
||||
f"PRO version must preserve all newlines, got: {repr(result)}"
|
||||
|
||||
@patch('src.utils.constants.VERSION_TYPE', 'free')
|
||||
def test_process_empty_text_in_free(self):
|
||||
"""FREE version should handle empty text gracefully"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
result = process_text_content("")
|
||||
|
||||
assert result == "", \
|
||||
f"Empty text should remain empty, got: {repr(result)}"
|
||||
|
||||
@patch('src.utils.constants.VERSION_TYPE', 'free')
|
||||
def test_process_text_without_newlines_in_free(self):
|
||||
"""FREE version should pass through text without newlines unchanged"""
|
||||
from src.core.text_processor import process_text_content
|
||||
|
||||
text = "Single line text"
|
||||
result = process_text_content(text)
|
||||
|
||||
assert result == "Single line text", \
|
||||
f"Text without newlines should be unchanged, got: {repr(result)}"
|
||||
assert result == text
|
||||
|
||||
@@ -28,22 +28,22 @@ class TestModalTextEditorOperator:
|
||||
assert hasattr(TEXT_TEXTURE_OT_edit_text, 'draw')
|
||||
|
||||
def test_poll_denies_access_in_free_version(self):
|
||||
"""poll() must return False when has_text_editor() is False (FREE version)."""
|
||||
"""poll() must return False when running in the free version."""
|
||||
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_text
|
||||
|
||||
mock_context = Mock()
|
||||
|
||||
with patch('src.operators.text_editor_ops.has_text_editor', return_value=False):
|
||||
with patch('src.operators.text_editor_ops.is_full_version', return_value=False):
|
||||
result = TEXT_TEXTURE_OT_edit_text.poll(mock_context)
|
||||
assert result is False, "FREE version should not have access to text editor"
|
||||
|
||||
def test_poll_allows_access_in_pro_version(self):
|
||||
"""poll() must return True when has_text_editor() is True (PRO version)."""
|
||||
"""poll() must return True when running in the pro version."""
|
||||
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_text
|
||||
|
||||
mock_context = Mock()
|
||||
|
||||
with patch('src.operators.text_editor_ops.has_text_editor', return_value=True):
|
||||
with patch('src.operators.text_editor_ops.is_full_version', return_value=True):
|
||||
result = TEXT_TEXTURE_OT_edit_text.poll(mock_context)
|
||||
assert result is True, "PRO version should have access to text editor"
|
||||
|
||||
@@ -125,4 +125,4 @@ class TestModalTextEditorOperator:
|
||||
from src.operators import text_editor_ops
|
||||
|
||||
assert hasattr(text_editor_ops, 'unregister')
|
||||
assert callable(text_editor_ops.unregister)
|
||||
assert callable(text_editor_ops.unregister)
|
||||
|
||||
@@ -9,28 +9,16 @@ src_path = Path(__file__).parent.parent.parent / "src"
|
||||
sys.path.insert(0, str(src_path))
|
||||
|
||||
|
||||
def test_has_multiline_text_function_complete():
|
||||
"""Test that has_multiline_text() is a complete function with body."""
|
||||
from utils.constants import has_multiline_text
|
||||
|
||||
# Should be callable
|
||||
assert callable(has_multiline_text)
|
||||
|
||||
# Should return a boolean (not crash with incomplete body)
|
||||
result = has_multiline_text()
|
||||
assert isinstance(result, bool)
|
||||
def test_has_multiline_text_not_importable():
|
||||
"""Ensure has_multiline_text was fully removed."""
|
||||
with pytest.raises(ImportError, match="cannot import name 'has_multiline_text'"):
|
||||
from utils.constants import has_multiline_text
|
||||
|
||||
|
||||
def test_has_text_editor_imports_from_constants():
|
||||
"""Test that has_text_editor can be imported from utils.constants."""
|
||||
from utils.constants import has_text_editor
|
||||
|
||||
# Verify it's callable (should be a function)
|
||||
assert callable(has_text_editor)
|
||||
|
||||
# Should return a boolean without crashing
|
||||
result = has_text_editor()
|
||||
assert isinstance(result, bool)
|
||||
def test_has_text_editor_not_importable():
|
||||
"""Ensure has_text_editor helper was removed with the multiline feature."""
|
||||
with pytest.raises(ImportError, match="cannot import name 'has_text_editor'"):
|
||||
from utils.constants import has_text_editor
|
||||
|
||||
|
||||
def test_new_operator_name_imports_from_operators():
|
||||
@@ -45,4 +33,4 @@ def test_new_operator_name_imports_from_operators():
|
||||
def test_old_operator_name_not_importable():
|
||||
"""Test that TEXT_TEXTURE_OT_edit_multiline_text (OLD name) is NOT importable."""
|
||||
with pytest.raises(ImportError, match="cannot import name 'TEXT_TEXTURE_OT_edit_multiline_text'"):
|
||||
from src.operators import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
from src.operators import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
|
||||
@@ -4,7 +4,7 @@ Tests the core text processing logic in isolation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -13,7 +13,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
|
||||
|
||||
from fixtures.sample_data import *
|
||||
from src.core.text_processor import (
|
||||
process_multiline_text,
|
||||
wrap_text_to_width,
|
||||
render_text_with_stroke,
|
||||
calculate_text_metrics,
|
||||
@@ -97,68 +96,6 @@ class TestWrapTextToWidth:
|
||||
assert "SuperLongWordThatCannotFit" in result
|
||||
|
||||
|
||||
class TestProcessMultilineText:
|
||||
"""Test the process_multiline_text function"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_processor.wrap_text_to_width')
|
||||
def test_process_single_line_text(self, mock_wrap, sample_font):
|
||||
"""Test processing single line text"""
|
||||
mock_wrap.return_value = [SHORT_TEXT]
|
||||
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 100, 20)
|
||||
|
||||
result = process_multiline_text(SHORT_TEXT, sample_font, 200, 100)
|
||||
|
||||
assert result['line_count'] == 1
|
||||
assert result['lines'] == [SHORT_TEXT]
|
||||
assert result['total_height'] == 20
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_processor.wrap_text_to_width')
|
||||
def test_process_multiline_text_basic(self, mock_wrap, sample_font):
|
||||
"""Test processing actual multiline text"""
|
||||
mock_wrap.side_effect = [['Line 1'], ['Line 2'], ['Line 3'], ['Line 4']]
|
||||
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 80, 20)
|
||||
|
||||
result = process_multiline_text(MULTILINE_TEXT, sample_font, 200, 200)
|
||||
|
||||
assert result['line_count'] == 4
|
||||
assert len(result['lines']) == 4
|
||||
assert result['total_height'] == 80 # 4 lines * 20 height
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_process_empty_text(self, sample_font):
|
||||
"""Test processing empty text"""
|
||||
result = process_multiline_text("", sample_font, 200, 100)
|
||||
|
||||
assert result['line_count'] == 1
|
||||
assert result['lines'] == [""]
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('src.core.text_processor.wrap_text_to_width')
|
||||
def test_process_height_limit_exceeded(self, mock_wrap, sample_font):
|
||||
"""Test text processing when height limit is exceeded"""
|
||||
mock_wrap.side_effect = [['Line 1'], ['Line 2'], ['Line 3'], ['Line 4']]
|
||||
|
||||
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_draw.textbbox.return_value = (0, 0, 80, 30) # Each line is 30px high
|
||||
|
||||
# With max_height=50, only 1 line should fit
|
||||
result = process_multiline_text(MULTILINE_TEXT, sample_font, 200, 50)
|
||||
|
||||
assert result['line_count'] <= 2 # Should stop when height limit reached
|
||||
assert result['total_height'] <= 60 # Should not exceed much
|
||||
|
||||
|
||||
class TestCalculateTextMetrics:
|
||||
"""Test the calculate_text_metrics function"""
|
||||
@@ -416,4 +353,4 @@ class TestOptimizeTextLayout:
|
||||
"""Test optimization with empty text blocks list"""
|
||||
result = optimize_text_layout([], 500, 400)
|
||||
|
||||
assert result == []
|
||||
assert result == []
|
||||
|
||||
@@ -129,26 +129,6 @@ class TestVersionDetection:
|
||||
patch('src.utils.constants.ENABLED_FEATURES', ['basic', 'preview', 'text_fitting']):
|
||||
assert has_text_fitting() is True
|
||||
|
||||
def test_has_multiline_text_returns_false_in_free_version(self):
|
||||
"""Multiline text is not available in free version."""
|
||||
from src.utils.constants import has_multiline_text
|
||||
from unittest.mock import patch
|
||||
|
||||
# Test free version - need to patch ENABLED_FEATURES since it's computed at import
|
||||
with patch('src.utils.constants.VERSION_TYPE', 'free'), \
|
||||
patch('src.utils.constants.ENABLED_FEATURES', ['basic', 'preview']):
|
||||
assert has_multiline_text() is False
|
||||
|
||||
def test_has_multiline_text_returns_true_in_pro_version(self):
|
||||
"""Multiline text is available in PRO version."""
|
||||
from src.utils.constants import has_multiline_text
|
||||
from unittest.mock import patch
|
||||
|
||||
# Test full version - need to patch ENABLED_FEATURES since it's computed at import
|
||||
with patch('src.utils.constants.VERSION_TYPE', 'full'), \
|
||||
patch('src.utils.constants.ENABLED_FEATURES', ['basic', 'preview', 'multiline_text']):
|
||||
assert has_multiline_text() is True
|
||||
|
||||
|
||||
|
||||
class TestRuntimeImportDetection:
|
||||
@@ -317,4 +297,4 @@ class TestVersionDifferentiationScenarios:
|
||||
# Generous technical limits
|
||||
limits = get_version_limits()
|
||||
assert limits["max_texture_size"] == 4096, "Should have 4096px texture limit"
|
||||
assert limits["max_concurrent_operations"] == 4, "Should have 4 concurrent operations limit"
|
||||
assert limits["max_concurrent_operations"] == 4, "Should have 4 concurrent operations limit"
|
||||
|
||||
Reference in New Issue
Block a user