text fitting pro feature
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -2,14 +2,6 @@
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add src to path for imports
|
||||
src_path = Path(__file__).parent.parent.parent.parent / "src"
|
||||
sys.path.insert(0, str(src_path))
|
||||
|
||||
from core.generation_engine import generate_texture_image
|
||||
|
||||
|
||||
class TestGenerateTextureImageControlFlow:
|
||||
@@ -49,8 +41,11 @@ class TestGenerateTextureImageControlFlow:
|
||||
mock_props.enable_text_fitting = False
|
||||
mock_props.generate_normal_map = False
|
||||
|
||||
# Import function with proper src path
|
||||
from src.core.generation_engine import generate_texture_image
|
||||
|
||||
# Patch bpy at module level with proper structure
|
||||
with patch('core.generation_engine.bpy') as mock_bpy:
|
||||
with patch('src.core.generation_engine.bpy') as mock_bpy:
|
||||
# Properly structure the bpy mock
|
||||
mock_images = Mock()
|
||||
mock_images.new = Mock(return_value=mock_blender_img)
|
||||
@@ -121,7 +116,10 @@ class TestGenerateTextureImageControlFlow:
|
||||
mock_props.enable_text_fitting = False
|
||||
mock_props.generate_normal_map = False
|
||||
|
||||
with patch('core.generation_engine.bpy') as mock_bpy:
|
||||
# Import function with proper src path
|
||||
from src.core.generation_engine import generate_texture_image
|
||||
|
||||
with patch('src.core.generation_engine.bpy') as mock_bpy:
|
||||
# Properly structure the bpy mock
|
||||
mock_images = Mock()
|
||||
mock_images.new = Mock(return_value=mock_blender_img)
|
||||
|
||||
442
tests/unit/test_build_minification.py
Normal file
442
tests/unit/test_build_minification.py
Normal file
@@ -0,0 +1,442 @@
|
||||
"""
|
||||
Unit tests for build system code minification features.
|
||||
|
||||
These tests verify that the build process can:
|
||||
1. Strip comments while preserving functionality
|
||||
2. Remove docstrings except bl_info and functionally required ones
|
||||
3. Optimize whitespace
|
||||
4. Maintain valid Python syntax and runtime behavior
|
||||
"""
|
||||
|
||||
import ast
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Add scripts directory to path to import build_addon
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "scripts"))
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_strips_inline_comments():
|
||||
"""Test that inline comments are removed from code."""
|
||||
source = textwrap.dedent("""
|
||||
def calculate_width(text, font_size):
|
||||
# Calculate the pixel width of text
|
||||
base_width = len(text) * font_size # Simple approximation
|
||||
return base_width * 0.6 # Adjustment factor
|
||||
""").strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# Verify comments are removed
|
||||
assert "# Calculate" not in result
|
||||
assert "# Simple approximation" not in result
|
||||
assert "# Adjustment factor" not in result
|
||||
|
||||
# Verify functionality is preserved
|
||||
assert "def calculate_width(text, font_size):" in result
|
||||
assert "base_width = len(text) * font_size" in result
|
||||
assert "return base_width * 0.6" in result
|
||||
|
||||
# Verify result is valid Python
|
||||
ast.parse(result)
|
||||
|
||||
|
||||
def test_strips_block_comments():
|
||||
"""Test that block comments at various positions are removed."""
|
||||
source = textwrap.dedent("""
|
||||
# Module-level comment about the purpose
|
||||
# Another line of module comment
|
||||
|
||||
def process_text(text):
|
||||
# Beginning of function comment
|
||||
result = text.upper()
|
||||
# Middle comment
|
||||
result = result.strip()
|
||||
# End comment
|
||||
return result
|
||||
""").strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# All comments should be gone
|
||||
assert "# Module-level" not in result
|
||||
assert "# Another line" not in result
|
||||
assert "# Beginning of function" not in result
|
||||
assert "# Middle comment" not in result
|
||||
assert "# End comment" not in result
|
||||
|
||||
# But code should remain
|
||||
assert "def process_text(text):" in result
|
||||
assert "result = text.upper()" in result
|
||||
assert "result = result.strip()" in result
|
||||
assert "return result" in result
|
||||
|
||||
|
||||
def test_preserves_bl_info_dict():
|
||||
"""Test that bl_info dictionary is preserved (required by Blender)."""
|
||||
source = textwrap.dedent('''
|
||||
"""
|
||||
Addon docstring that should be removed.
|
||||
"""
|
||||
|
||||
bl_info = {
|
||||
"name": "Text Texture Generator",
|
||||
"author": "Developer",
|
||||
"version": (1, 0, 0),
|
||||
"blender": (4, 0, 0),
|
||||
"description": "Generate text textures",
|
||||
}
|
||||
|
||||
def some_function():
|
||||
"""Regular docstring to remove."""
|
||||
return "value"
|
||||
''').strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# bl_info must be preserved exactly
|
||||
assert "bl_info = {" in result
|
||||
assert '"name": "Text Texture Generator"' in result
|
||||
assert '"author": "Developer"' in result
|
||||
assert '"version": (1, 0, 0)' in result
|
||||
|
||||
# Module docstring should be removed
|
||||
assert "Addon docstring that should be removed" not in result
|
||||
|
||||
# Function docstring should be removed
|
||||
assert "Regular docstring to remove" not in result
|
||||
|
||||
# Code should remain
|
||||
assert "def some_function():" in result
|
||||
assert 'return "value"' in result
|
||||
|
||||
|
||||
def test_removes_function_docstrings():
|
||||
"""Test that function and method docstrings are removed."""
|
||||
source = textwrap.dedent('''
|
||||
class TextGenerator:
|
||||
"""Class docstring to remove."""
|
||||
|
||||
def generate(self, text):
|
||||
"""
|
||||
Generate a texture from text.
|
||||
|
||||
Args:
|
||||
text: The input text string
|
||||
|
||||
Returns:
|
||||
Generated texture object
|
||||
"""
|
||||
return self._process(text)
|
||||
|
||||
def _process(self, text):
|
||||
"""Internal processing method."""
|
||||
return text.upper()
|
||||
''').strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# All docstrings should be removed
|
||||
assert "Class docstring to remove" not in result
|
||||
assert "Generate a texture from text" not in result
|
||||
assert "Args:" not in result
|
||||
assert "Returns:" not in result
|
||||
assert "Internal processing method" not in result
|
||||
|
||||
# Code structure should remain
|
||||
assert "class TextGenerator:" in result
|
||||
assert "def generate(self, text):" in result
|
||||
assert "return self._process(text)" in result
|
||||
assert "def _process(self, text):" in result
|
||||
assert "return text.upper()" in result
|
||||
|
||||
|
||||
def test_preserves_string_literals():
|
||||
"""Test that string literals used as data are not removed."""
|
||||
source = textwrap.dedent('''
|
||||
def get_error_messages():
|
||||
"""Function docstring to remove."""
|
||||
# Comment to remove
|
||||
errors = {
|
||||
"invalid_input": "Input text cannot be empty",
|
||||
"font_missing": "Selected font not found",
|
||||
}
|
||||
return errors
|
||||
|
||||
def validate(text):
|
||||
# Check if text is valid
|
||||
if not text:
|
||||
raise ValueError("Text cannot be empty") # Inline comment
|
||||
return True
|
||||
''').strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# Docstrings and comments should be removed
|
||||
assert "Function docstring to remove" not in result
|
||||
assert "# Comment to remove" not in result
|
||||
assert "# Check if text is valid" not in result
|
||||
assert "# Inline comment" not in result
|
||||
|
||||
# String literals in code must be preserved
|
||||
assert '"invalid_input": "Input text cannot be empty"' in result
|
||||
assert '"font_missing": "Selected font not found"' in result
|
||||
assert 'raise ValueError("Text cannot be empty")' in result
|
||||
|
||||
# Code structure preserved
|
||||
assert "def get_error_messages():" in result
|
||||
assert "def validate(text):" in result
|
||||
|
||||
|
||||
def test_optimizes_whitespace():
|
||||
"""Test that excessive blank lines and inconsistent indentation are cleaned up."""
|
||||
source = textwrap.dedent("""
|
||||
def function_one():
|
||||
result = 1
|
||||
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def function_two():
|
||||
# Comment
|
||||
|
||||
|
||||
value = 2
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class MyClass:
|
||||
|
||||
|
||||
def method(self):
|
||||
pass
|
||||
""").strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# Should not have excessive blank lines (max 1 between definitions)
|
||||
assert "\n\n\n" not in result
|
||||
|
||||
# Functions should be separated by at most one blank line
|
||||
lines = result.split("\n")
|
||||
blank_line_count = 0
|
||||
max_consecutive_blanks = 0
|
||||
for line in lines:
|
||||
if line.strip() == "":
|
||||
blank_line_count += 1
|
||||
max_consecutive_blanks = max(max_consecutive_blanks, blank_line_count)
|
||||
else:
|
||||
blank_line_count = 0
|
||||
|
||||
assert max_consecutive_blanks <= 1, f"Found {max_consecutive_blanks} consecutive blank lines"
|
||||
|
||||
# Code should still be present
|
||||
assert "def function_one():" in result
|
||||
assert "def function_two():" in result
|
||||
assert "class MyClass:" in result
|
||||
|
||||
|
||||
def test_maintains_valid_python_syntax():
|
||||
"""Test that minified code is syntactically valid Python."""
|
||||
source = textwrap.dedent('''
|
||||
"""Module docstring."""
|
||||
|
||||
# Import comment
|
||||
import bpy # Blender Python API
|
||||
from typing import Optional # Type hints
|
||||
|
||||
class TextureGenerator:
|
||||
"""Generator class."""
|
||||
|
||||
def __init__(self, resolution=1024):
|
||||
"""Initialize generator."""
|
||||
# Store resolution
|
||||
self.resolution = resolution # Texture size
|
||||
|
||||
def generate(self, text: str) -> Optional[object]:
|
||||
"""
|
||||
Generate texture from text.
|
||||
|
||||
Args:
|
||||
text: Input text string
|
||||
|
||||
Returns:
|
||||
Texture object or None
|
||||
"""
|
||||
if not text: # Validate input
|
||||
return None
|
||||
|
||||
# Create texture
|
||||
texture = bpy.data.images.new(
|
||||
"TextTexture",
|
||||
self.resolution,
|
||||
self.resolution
|
||||
)
|
||||
|
||||
return texture # Return result
|
||||
''').strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# Must be valid Python
|
||||
try:
|
||||
ast.parse(result)
|
||||
except SyntaxError as e:
|
||||
pytest.fail(f"Minified code has syntax error: {e}")
|
||||
|
||||
# Should not have docstrings or comments
|
||||
assert '"""Module docstring."""' not in result
|
||||
assert '"""Generator class."""' not in result
|
||||
assert '"""Initialize generator."""' not in result
|
||||
assert "# Import comment" not in result
|
||||
assert "# Blender Python API" not in result
|
||||
assert "# Store resolution" not in result
|
||||
|
||||
# Code should be present
|
||||
assert "import bpy" in result
|
||||
assert "from typing import Optional" in result
|
||||
assert "class TextureGenerator:" in result
|
||||
assert "def __init__(self, resolution=1024):" in result
|
||||
assert "self.resolution = resolution" in result
|
||||
|
||||
|
||||
def test_preserves_strings_with_hash_symbols():
|
||||
"""Test that strings containing hash symbols are not treated as comments."""
|
||||
source = textwrap.dedent('''
|
||||
def create_id(text):
|
||||
"""Generate hash ID.""" # Function comment
|
||||
# Create the ID with a hash prefix
|
||||
return f"#{hash(text)}" # Hash prefix
|
||||
|
||||
def parse_color(color_string):
|
||||
# Check if it's a hex color
|
||||
if color_string.startswith("#"): # Hex colors start with #
|
||||
return color_string
|
||||
return None
|
||||
''').strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# Comments should be removed
|
||||
assert "Function comment" not in result
|
||||
assert "# Create the ID" not in result
|
||||
assert "# Hash prefix" not in result
|
||||
assert "# Check if it's a hex color" not in result
|
||||
assert "# Hex colors start with #" not in result
|
||||
|
||||
# String literals with # should be preserved
|
||||
assert 'f"#{hash(text)}"' in result or 'f"#{hash(text)}"' in result
|
||||
assert 'startswith("#")' in result or 'startswith("#")' in result
|
||||
|
||||
# Code structure preserved
|
||||
assert "def create_id(text):" in result
|
||||
assert "def parse_color(color_string):" in result
|
||||
|
||||
|
||||
def test_preserves_functionality_complex_example():
|
||||
"""Test minification preserves functionality in complex real-world code."""
|
||||
source = textwrap.dedent('''
|
||||
"""
|
||||
Text processing module for the addon.
|
||||
|
||||
This module handles text wrapping and fitting.
|
||||
"""
|
||||
|
||||
import re # Regular expressions
|
||||
from typing import List, Tuple # Type hints
|
||||
|
||||
class TextProcessor:
|
||||
"""Process text for texture generation."""
|
||||
|
||||
def __init__(self, max_width: int = 100):
|
||||
"""
|
||||
Initialize processor.
|
||||
|
||||
Args:
|
||||
max_width: Maximum line width in pixels
|
||||
"""
|
||||
# Store configuration
|
||||
self.max_width = max_width # Width limit
|
||||
self._cache = {} # Results cache
|
||||
|
||||
def wrap_text(self, text: str) -> List[str]:
|
||||
"""
|
||||
Wrap text to fit within max width.
|
||||
|
||||
Args:
|
||||
text: Input text to wrap
|
||||
|
||||
Returns:
|
||||
List of wrapped lines
|
||||
"""
|
||||
# Check cache first
|
||||
if text in self._cache: # Cache hit
|
||||
return self._cache[text]
|
||||
|
||||
# Split into words
|
||||
words = re.split(r'\\s+', text) # Word boundaries
|
||||
lines = []
|
||||
current_line = ""
|
||||
|
||||
# Process each word
|
||||
for word in words:
|
||||
# Check if adding word exceeds width
|
||||
test_line = f"{current_line} {word}".strip()
|
||||
if len(test_line) <= self.max_width: # Fits
|
||||
current_line = test_line
|
||||
else: # Doesn't fit
|
||||
if current_line: # Save current line
|
||||
lines.append(current_line)
|
||||
current_line = word # Start new line
|
||||
|
||||
# Add final line
|
||||
if current_line: # Not empty
|
||||
lines.append(current_line)
|
||||
|
||||
# Cache result
|
||||
self._cache[text] = lines # Store for reuse
|
||||
return lines
|
||||
''').strip()
|
||||
|
||||
from build_addon import minify_python_code
|
||||
result = minify_python_code(source)
|
||||
|
||||
# Must be valid Python
|
||||
try:
|
||||
compiled = compile(result, '<minified>', 'exec')
|
||||
except SyntaxError as e:
|
||||
pytest.fail(f"Minified code won't compile: {e}")
|
||||
|
||||
# Verify it's executable (can define the class)
|
||||
namespace = {}
|
||||
exec(compiled, namespace)
|
||||
assert 'TextProcessor' in namespace
|
||||
|
||||
# Verify functionality works
|
||||
processor = namespace['TextProcessor'](max_width=20)
|
||||
result_lines = processor.wrap_text("hello world test")
|
||||
assert isinstance(result_lines, list)
|
||||
assert len(result_lines) > 0
|
||||
|
||||
# No docstrings or comments should remain
|
||||
minified_lines = result.split('\n')
|
||||
for line in minified_lines:
|
||||
stripped = line.strip()
|
||||
if stripped:
|
||||
assert not stripped.startswith('#'), f"Found comment line: {line}"
|
||||
assert '"""' not in line, f"Found docstring in: {line}"
|
||||
@@ -114,6 +114,21 @@ class TestVersionDetection:
|
||||
assert limits["max_concurrent_operations"] == 1, \
|
||||
f"Expected free version max_concurrent_operations=1, got {limits['max_concurrent_operations']}"
|
||||
|
||||
def test_has_text_fitting_returns_correct_value_per_version(self):
|
||||
"""Test that has_text_fitting() returns correct value based on VERSION_TYPE."""
|
||||
from src.utils.constants import has_text_fitting
|
||||
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_text_fitting() is False
|
||||
|
||||
# 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', 'text_fitting']):
|
||||
assert has_text_fitting() is True
|
||||
|
||||
|
||||
|
||||
class TestRuntimeImportDetection:
|
||||
|
||||
Reference in New Issue
Block a user