442 lines
14 KiB
Python
442 lines
14 KiB
Python
"""
|
|
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}" |