remove multiline

This commit is contained in:
2025-10-13 00:10:24 +02:00
parent 97950f44c1
commit ad55572aa8
21 changed files with 112 additions and 943 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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 == []

View File

@@ -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"