feat: Complete Issue #65 Template Engine Foundation + Fix CLI Regression
## Issue #65 - Template Engine Foundation (COMPLETED) - Implement complete TDD8 methodology with 30 comprehensive tests (100% passing) - Add template variable parser with Unicode and dot notation support - Add template rendering engine with strict/lenient modes - Add business document generation (invoices, reports) - Add CLI integration with `markitect template-render` command - Add performance optimization (1000+ variables in <0.1s) ## Critical CLI Regression Fix - Fix broken `markitect --help` due to import path issues in markitect/issues/base.py - Add proper path resolution for domain module accessibility - Add 12 comprehensive CLI integration tests to prevent future regressions - Restore full CLI functionality with 35+ working commands ## Template Engine Architecture - markitect/template/parser.py - Variable parsing with comprehensive validation - markitect/template/engine.py - Template rendering with business logic - markitect/template/__init__.py - Structured package exports - Comprehensive exception hierarchy for robust error handling ## Test Coverage Excellence - 30 Issue #65 tests: parser (9), substitution (14), integration (7) - 12 CLI integration tests for regression prevention - Business scenario validation with real invoice/report generation - Performance benchmarking and error handling validation ## CLI Professional Enhancement - Add template-render command with comprehensive options - Fix import path issues preventing CLI access - Add validation, data checking, output options - Support JSON/YAML data formats with auto-detection ## Business Impact - Transform MarkiTect from document analysis to business automation platform - Enable professional invoice and report generation - Provide robust CLI interface for document workflows - Establish foundation for Epic #64 advanced template features 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
346
tests/test_issue_65_template_substitution.py
Normal file
346
tests/test_issue_65_template_substitution.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
Test for Issue #65: Template Engine Foundation - Variable Substitution
|
||||
|
||||
This test module validates the template variable substitution functionality
|
||||
for the MarkiTect template engine, implementing TDD8 Cycle 2.
|
||||
|
||||
Tests focus on:
|
||||
- Basic variable substitution with data
|
||||
- Nested object access with dot notation
|
||||
- Missing variable handling (strict vs lenient modes)
|
||||
- Error handling and validation
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class TestTemplateVariableSubstitution:
|
||||
"""Test suite for template variable substitution functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment for each test."""
|
||||
# Import the template engine (will be implemented)
|
||||
try:
|
||||
from markitect.template.engine import TemplateEngine
|
||||
self.engine = TemplateEngine()
|
||||
except ImportError:
|
||||
self.engine = None
|
||||
|
||||
def test_substitute_simple_variables(self):
|
||||
"""Test basic variable substitution from template strings.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
TDD Phase: RED (test should fail initially)
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Hello {{name}}!"
|
||||
data = {"name": "Alice"}
|
||||
expected_result = "Hello Alice!"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_nested_variables(self):
|
||||
"""Test nested object variable substitution with dot notation.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
TDD Phase: RED (test should fail initially)
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Customer: {{customer.name}}, Email: {{customer.contact.email}}"
|
||||
data = {
|
||||
"customer": {
|
||||
"name": "Acme Corp",
|
||||
"contact": {
|
||||
"email": "info@acme.example"
|
||||
}
|
||||
}
|
||||
}
|
||||
expected_result = "Customer: Acme Corp, Email: info@acme.example"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_multiple_variables(self):
|
||||
"""Test substitution of multiple variables in same template.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Invoice {{invoice_number}} for {{customer.name}} - Total: {{total}} {{currency}}"
|
||||
data = {
|
||||
"invoice_number": "INV-2025-001",
|
||||
"customer": {"name": "Acme Corp"},
|
||||
"total": 1500.00,
|
||||
"currency": "EUR"
|
||||
}
|
||||
expected_result = "Invoice INV-2025-001 for Acme Corp - Total: 1500.0 EUR"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_missing_variable_strict_mode(self):
|
||||
"""Test handling of missing variables in strict mode (should raise error).
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Hello {{name}}, welcome to {{missing}}!"
|
||||
data = {"name": "Alice"}
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
# Strict mode should raise an exception for missing variables
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
self.engine.render(template_text, data, strict=True)
|
||||
|
||||
assert "missing" in str(exc_info.value).lower()
|
||||
|
||||
def test_substitute_missing_variable_lenient_mode(self):
|
||||
"""Test handling of missing variables in lenient mode (preserve placeholder).
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Hello {{name}}, welcome to {{missing}}!"
|
||||
data = {"name": "Alice"}
|
||||
expected_result = "Hello Alice, welcome to {{missing}}!"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data, strict=False)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_empty_template(self):
|
||||
"""Test substitution with template containing no variables.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "This is a regular markdown document with no variables."
|
||||
data = {"name": "Alice"}
|
||||
expected_result = template_text # Should remain unchanged
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_with_markdown_formatting(self):
|
||||
"""Test that markdown formatting is preserved during substitution.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = """---
|
||||
title: "Invoice {{invoice_number}}"
|
||||
---
|
||||
|
||||
# Invoice {{invoice_number}}
|
||||
|
||||
**Bill To**: {{customer.name}}
|
||||
*Email*: {{customer.email}}
|
||||
|
||||
## Summary
|
||||
- Total: {{total}}
|
||||
- Currency: {{currency}}
|
||||
"""
|
||||
data = {
|
||||
"invoice_number": "INV-2025-001",
|
||||
"customer": {
|
||||
"name": "Acme Corp",
|
||||
"email": "billing@acme.example"
|
||||
},
|
||||
"total": 1500.00,
|
||||
"currency": "EUR"
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
|
||||
# Check that markdown structure is preserved
|
||||
assert "---" in result # Frontmatter delimiters
|
||||
assert "# Invoice INV-2025-001" in result # Header with substituted value
|
||||
assert "**Bill To**: Acme Corp" in result # Bold formatting preserved
|
||||
assert "*Email*: billing@acme.example" in result # Italic formatting preserved
|
||||
assert "- Total: 1500.0" in result # List formatting preserved
|
||||
|
||||
def test_substitute_duplicate_variables(self):
|
||||
"""Test that duplicate variables are all substituted correctly.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "{{name}} says hello to {{name}} and {{company}}"
|
||||
data = {"name": "Alice", "company": "Acme Corp"}
|
||||
expected_result = "Alice says hello to Alice and Acme Corp"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_with_special_characters(self):
|
||||
"""Test substitution with special characters and unicode.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Grüße {{name}}, Café {{café.price}} €"
|
||||
data = {
|
||||
"name": "München",
|
||||
"café": {"price": "3.50"}
|
||||
}
|
||||
expected_result = "Grüße München, Café 3.50 €"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
class TestTemplateSubstitutionEdgeCases:
|
||||
"""Test edge cases and error conditions for template substitution."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment."""
|
||||
try:
|
||||
from markitect.template.engine import TemplateEngine
|
||||
self.engine = TemplateEngine()
|
||||
except ImportError:
|
||||
self.engine = None
|
||||
|
||||
def test_substitute_deeply_nested_objects(self):
|
||||
"""Test substitution with deeply nested object access.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "{{level1.level2.level3.level4.value}}"
|
||||
data = {
|
||||
"level1": {
|
||||
"level2": {
|
||||
"level3": {
|
||||
"level4": {
|
||||
"value": "deep_value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
expected_result = "deep_value"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_with_none_values(self):
|
||||
"""Test substitution when data contains None values.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Value: {{value}}, None: {{none_value}}"
|
||||
data = {"value": "exists", "none_value": None}
|
||||
expected_result = "Value: exists, None: None"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_with_numeric_types(self):
|
||||
"""Test substitution with various numeric data types.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Int: {{int_val}}, Float: {{float_val}}, Bool: {{bool_val}}"
|
||||
data = {
|
||||
"int_val": 42,
|
||||
"float_val": 3.14159,
|
||||
"bool_val": True
|
||||
}
|
||||
expected_result = "Int: 42, Float: 3.14159, Bool: True"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
result = self.engine.render(template_text, data)
|
||||
assert result == expected_result
|
||||
|
||||
def test_substitute_performance_large_template(self):
|
||||
"""Test substitution performance with large templates.
|
||||
|
||||
Reference: Issue #65 - Performance Requirements
|
||||
"""
|
||||
# Arrange
|
||||
variables = [f"{{{{field_{i}}}}}" for i in range(100)]
|
||||
template_text = " ".join(variables)
|
||||
data = {f"field_{i}": f"value_{i}" for i in range(100)}
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
import time
|
||||
start_time = time.time()
|
||||
result = self.engine.render(template_text, data)
|
||||
render_time = time.time() - start_time
|
||||
|
||||
# Performance requirement: <50ms for 100+ variables
|
||||
assert render_time < 0.05
|
||||
assert "value_0" in result
|
||||
assert "value_99" in result
|
||||
|
||||
def test_substitute_invalid_data_type(self):
|
||||
"""Test error handling when data is not a dictionary.
|
||||
|
||||
Reference: Issue #65 - Template Engine Foundation
|
||||
"""
|
||||
# Arrange
|
||||
template_text = "Hello {{name}}!"
|
||||
invalid_data = "not a dictionary"
|
||||
|
||||
# Act & Assert
|
||||
if self.engine is None:
|
||||
pytest.skip("TemplateEngine not implemented yet - TDD RED phase")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
self.engine.render(template_text, invalid_data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
Reference in New Issue
Block a user