text fitting pro feature
This commit is contained in:
Binary file not shown.
@@ -15,7 +15,7 @@ def test_built_package_bl_info_is_parseable():
|
||||
|
||||
# Step 1: Rebuild the package to ensure latest code
|
||||
result = subprocess.run(
|
||||
["python", "build/build_addon.py", "--type", "full"],
|
||||
["python", "scripts/build_addon.py"],
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
@@ -69,4 +69,39 @@ def test_built_package_bl_info_is_parseable():
|
||||
print('\n'.join(content.split('\n')[:30]))
|
||||
raise AssertionError(f"bl_info failed ast.literal_eval() at line {node.lineno}: {e}")
|
||||
|
||||
assert bl_info_found, "bl_info not found in __init__.py"
|
||||
assert bl_info_found, "bl_info not found in __init__.py"
|
||||
|
||||
|
||||
def test_manifest_not_in_source():
|
||||
"""Verify blender_manifest.toml is NOT in source directory (legacy mode)."""
|
||||
manifest_path = Path(__file__).parent.parent.parent / "src" / "blender_manifest.toml"
|
||||
assert not manifest_path.exists(), \
|
||||
"blender_manifest.toml should not exist in src/ for legacy addon mode"
|
||||
|
||||
|
||||
def test_manifest_not_in_built_package():
|
||||
"""Verify blender_manifest.toml is NOT in built packages (both FREE and FULL)."""
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
|
||||
# Check both FREE and FULL versions
|
||||
zip_files = [
|
||||
project_root / "dist" / "text_texture_generator_v1.0.0.zip", # FULL
|
||||
project_root / "dist" / "text_texture_generator_v1.0.0_free.zip", # FREE
|
||||
]
|
||||
|
||||
for zip_path in zip_files:
|
||||
if not zip_path.exists():
|
||||
continue # Skip if not built yet
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'r') as z:
|
||||
files = z.namelist()
|
||||
manifest_files = [f for f in files if 'blender_manifest.toml' in f]
|
||||
assert len(manifest_files) == 0, \
|
||||
f"blender_manifest.toml should not be in {zip_path.name}. Found: {manifest_files}"
|
||||
|
||||
|
||||
def test_manifest_template_not_exists():
|
||||
"""Verify blender_manifest.toml.template doesn't exist (prevents regeneration)."""
|
||||
template_path = Path(__file__).parent.parent.parent / "build" / "templates" / "blender_manifest.toml.template"
|
||||
assert not template_path.exists(), \
|
||||
"blender_manifest.toml.template should not exist to prevent automatic regeneration during builds"
|
||||
@@ -347,7 +347,66 @@ print("SUCCESS: Image overlay properly blocked")
|
||||
output = test_result.output.decode('utf-8', errors='replace')
|
||||
|
||||
assert test_result.exit_code == 0, f"Overlay block test failed:\n{output}"
|
||||
assert "SUCCESS: Image overlay properly blocked" in output
|
||||
|
||||
|
||||
def test_free_version_blocks_text_fitting(blender_container, addon_package):
|
||||
"""
|
||||
SPECIFICATION: Free version MUST block text fitting functionality.
|
||||
|
||||
Expected behavior (VERSION_TYPE="free"):
|
||||
- "text_fitting" NOT in ENABLED_FEATURES
|
||||
- has_text_fitting() returns False
|
||||
- Text fitting controls unavailable
|
||||
- UI shows "🔒 PRO" on text fitting controls
|
||||
|
||||
Current state (VERSION_TYPE="full"):
|
||||
- Test SKIPS (text_fitting is available)
|
||||
"""
|
||||
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 text_fitting NOT enabled
|
||||
from text_texture_generator.utils.constants import ENABLED_FEATURES, has_text_fitting
|
||||
if "text_fitting" in ENABLED_FEATURES:
|
||||
print(f"FAIL: text_fitting should NOT be in ENABLED_FEATURES for free version")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify has_text_fitting() returns False
|
||||
if has_text_fitting():
|
||||
print(f"FAIL: has_text_fitting() should return False for free version")
|
||||
sys.exit(1)
|
||||
|
||||
print("SUCCESS: Text fitting properly blocked")
|
||||
"""
|
||||
|
||||
tar_stream = io.BytesIO()
|
||||
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
|
||||
script_info = tarfile.TarInfo(name='test_text_fitting.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_text_fitting.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"Text fitting block test failed:\n{output}"
|
||||
assert "SUCCESS: Text fitting properly blocked" in output
|
||||
|
||||
|
||||
def test_free_version_texture_size_limited(blender_container, addon_package):
|
||||
|
||||
159
tests/integration/test_build_minification_integration.py
Normal file
159
tests/integration/test_build_minification_integration.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Integration tests for Python code minification during addon build process.
|
||||
Tests verify that the build system correctly applies minification when requested.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# Add scripts to path to import build module
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "scripts"))
|
||||
|
||||
from build_addon import build_addon, minify_python_code
|
||||
|
||||
|
||||
class TestBuildMinificationIntegration:
|
||||
"""Integration tests for minification in the build process"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_build_env(self):
|
||||
"""Create a temporary build environment with sample Python files"""
|
||||
# Create temp directories
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
project_root = Path(temp_dir)
|
||||
src_dir = project_root / "src"
|
||||
build_dir = project_root / "build"
|
||||
output_dir = project_root / "dist"
|
||||
|
||||
src_dir.mkdir(parents=True)
|
||||
build_dir.mkdir(parents=True)
|
||||
output_dir.mkdir(parents=True)
|
||||
|
||||
# Create sample Python file with comments and docstrings
|
||||
sample_code = '''"""Module docstring to be removed"""
|
||||
# This is a comment
|
||||
def hello_world():
|
||||
"""Function docstring to be removed"""
|
||||
# Inline comment
|
||||
return "Hello, World!"
|
||||
|
||||
class MyClass:
|
||||
"""Class docstring to be removed"""
|
||||
def method(self):
|
||||
# Another comment
|
||||
return 42
|
||||
'''
|
||||
|
||||
(src_dir / "__init__.py").write_text(sample_code)
|
||||
(src_dir / "blender_manifest.toml").write_text('[manifest]\nname = "Test"\n')
|
||||
|
||||
# Create minimal config files
|
||||
config = {
|
||||
"project": {"id": "test_addon"},
|
||||
"build": {
|
||||
"source_dir": "src",
|
||||
"dist_dir": "dist",
|
||||
"temp_dir": "build/temp",
|
||||
"exclude_patterns": ["__pycache__", "*.pyc"]
|
||||
}
|
||||
}
|
||||
|
||||
exclusions = {
|
||||
"version_configs": {
|
||||
"full": {"exclude_files": [], "exclude_patterns": []},
|
||||
"free": {"exclude_files": [], "exclude_patterns": []}
|
||||
}
|
||||
}
|
||||
|
||||
(build_dir / "config.json").write_text(json.dumps(config))
|
||||
(build_dir / "file_exclusions.json").write_text(json.dumps(exclusions))
|
||||
|
||||
# Create dummy sync_version.py
|
||||
sync_script = '''#!/usr/bin/env python3
|
||||
import sys
|
||||
if "--version" in sys.argv:
|
||||
print("Synchronizing version: 1.0.0")
|
||||
sys.exit(0)
|
||||
'''
|
||||
(build_dir / "sync_version.py").write_text(sync_script)
|
||||
|
||||
yield {
|
||||
'project_root': project_root,
|
||||
'src_dir': src_dir,
|
||||
'output_dir': output_dir,
|
||||
'sample_code': sample_code
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
def test_build_with_minify_flag_produces_minified_python_files(self, temp_build_env, monkeypatch):
|
||||
"""Test that build with minify=True produces minified .py files in output"""
|
||||
# This test will fail because minify parameter doesn't exist in build_addon()
|
||||
|
||||
project_root = temp_build_env['project_root']
|
||||
output_dir = temp_build_env['output_dir']
|
||||
|
||||
# Change to project root for build
|
||||
monkeypatch.chdir(project_root)
|
||||
|
||||
# Build with minification enabled
|
||||
# Expected: build_addon() should accept minify=True parameter
|
||||
output_zip = build_addon(
|
||||
version_type="full",
|
||||
version="1.0.0",
|
||||
output_dir=output_dir,
|
||||
minify=True # This parameter doesn't exist yet - will cause TypeError
|
||||
)
|
||||
|
||||
# Extract and verify minified content
|
||||
with zipfile.ZipFile(output_zip, 'r') as zf:
|
||||
# Find the __init__.py file in the zip
|
||||
init_files = [name for name in zf.namelist() if name.endswith('__init__.py')]
|
||||
assert len(init_files) > 0, "No __init__.py found in package"
|
||||
|
||||
minified_content = zf.read(init_files[0]).decode('utf-8')
|
||||
|
||||
# Verify minification occurred
|
||||
assert '# This is a comment' not in minified_content, "Comments should be removed"
|
||||
assert '"""Module docstring to be removed"""' not in minified_content, "Module docstrings should be removed"
|
||||
assert '"""Function docstring to be removed"""' not in minified_content, "Function docstrings should be removed"
|
||||
assert 'def hello_world():' in minified_content, "Function definition should be preserved"
|
||||
assert 'return "Hello, World!"' in minified_content, "Function body should be preserved"
|
||||
|
||||
def test_build_without_minify_flag_preserves_original_code(self, temp_build_env, monkeypatch):
|
||||
"""Test that build with minify=False preserves original Python code"""
|
||||
# This test will fail because minify parameter doesn't exist
|
||||
|
||||
project_root = temp_build_env['project_root']
|
||||
output_dir = temp_build_env['output_dir']
|
||||
original_code = temp_build_env['sample_code']
|
||||
|
||||
monkeypatch.chdir(project_root)
|
||||
|
||||
# Build with minification disabled
|
||||
output_zip = build_addon(
|
||||
version_type="full",
|
||||
version="1.0.0",
|
||||
output_dir=output_dir,
|
||||
minify=False # This parameter doesn't exist yet - will cause TypeError
|
||||
)
|
||||
|
||||
# Extract and verify original content preserved
|
||||
with zipfile.ZipFile(output_zip, 'r') as zf:
|
||||
init_files = [name for name in zf.namelist() if name.endswith('__init__.py')]
|
||||
assert len(init_files) > 0
|
||||
|
||||
preserved_content = zf.read(init_files[0]).decode('utf-8')
|
||||
|
||||
# Verify original code is intact
|
||||
assert '# This is a comment' in preserved_content, "Comments should be preserved"
|
||||
assert '"""Module docstring to be removed"""' in preserved_content, "Docstrings should be preserved"
|
||||
assert 'def hello_world():' in preserved_content
|
||||
assert 'return "Hello, World!"' in preserved_content
|
||||
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