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:
2025-10-02 15:33:32 +02:00
parent d0c36befb3
commit bcbe78d04f
12 changed files with 2341 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
"""
CLI Integration Tests - Prevent CLI Entry Point Regressions
This test module validates that the CLI entry point is properly accessible
and core commands work as expected. It prevents regressions like broken
imports or missing entry points that would break user accessibility.
Tests focus on:
- CLI entry point accessibility (markitect --help)
- Core command availability and help text
- Template rendering CLI functionality
- Error handling in CLI commands
"""
import subprocess
import tempfile
import json
import os
import pytest
from pathlib import Path
class TestCLIEntryPoint:
"""Test CLI entry point accessibility."""
def test_markitect_help_accessible(self):
"""Test that markitect --help works and shows expected content.
This prevents regressions where import errors break CLI accessibility.
"""
# Run markitect --help
result = subprocess.run(
['markitect', '--help'],
capture_output=True,
text=True
)
# Should exit successfully
assert result.returncode == 0, f"CLI help failed with error: {result.stderr}"
# Should contain core CLI information
output = result.stdout
assert "MarkiTect - Advanced Markdown engine" in output
assert "Commands:" in output
assert "--help" in output
# Should not have import errors
assert "ModuleNotFoundError" not in result.stderr
assert "ImportError" not in result.stderr
def test_core_commands_available(self):
"""Test that core commands are listed in help output."""
result = subprocess.run(
['markitect', '--help'],
capture_output=True,
text=True
)
output = result.stdout
# Core functionality commands
assert "ingest" in output
assert "list" in output
assert "status" in output or "stats" in output
# Template engine command (Issue #65)
assert "template-render" in output
# Schema commands
assert "schema-generate" in output
assert "validate" in output
def test_template_render_command_help(self):
"""Test that template-render command help is accessible."""
result = subprocess.run(
['markitect', 'template-render', '--help'],
capture_output=True,
text=True
)
assert result.returncode == 0
output = result.stdout
assert "Render a template with data" in output
assert "TEMPLATE_FILE" in output
assert "DATA_FILE" in output
assert "--output" in output
assert "--strict" in output
assert "--lenient" in output
class TestTemplateRenderCLI:
"""Test template-render CLI functionality end-to-end."""
def test_template_render_basic_functionality(self):
"""Test basic template rendering via CLI."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create test template
template_file = temp_path / "test.md"
template_file.write_text("# {{title}}\n\nHello {{name}}!")
# Create test data
data_file = temp_path / "data.json"
data = {"title": "Test Document", "name": "World"}
data_file.write_text(json.dumps(data))
# Run template rendering
result = subprocess.run(
['markitect', 'template-render', str(template_file), str(data_file)],
capture_output=True,
text=True
)
assert result.returncode == 0, f"Template rendering failed: {result.stderr}"
output = result.stdout
assert "# Test Document" in output
assert "Hello World!" in output
def test_template_render_with_output_file(self):
"""Test template rendering with output file option."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create test files
template_file = temp_path / "template.md"
template_file.write_text("Result: {{value}}")
data_file = temp_path / "data.json"
data_file.write_text(json.dumps({"value": "SUCCESS"}))
output_file = temp_path / "output.md"
# Run with output option
result = subprocess.run(
['markitect', 'template-render',
str(template_file), str(data_file),
'--output', str(output_file)],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "Template rendered successfully" in result.stdout
# Check output file was created
assert output_file.exists()
content = output_file.read_text()
assert "Result: SUCCESS" in content
def test_template_render_validation_mode(self):
"""Test template rendering with validation options."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create valid template
template_file = temp_path / "valid.md"
template_file.write_text("Valid: {{name}}")
data_file = temp_path / "data.json"
data_file.write_text(json.dumps({"name": "test"}))
# Run with validation
result = subprocess.run(
['markitect', 'template-render',
str(template_file), str(data_file),
'--validate', '--check-data'],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "Valid: test" in result.stdout
def test_template_render_error_handling(self):
"""Test CLI error handling for invalid inputs."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Test with non-existent template file
result = subprocess.run(
['markitect', 'template-render', 'nonexistent.md', 'data.json'],
capture_output=True,
text=True
)
assert result.returncode != 0
assert "does not exist" in result.stderr.lower() or "not found" in result.stderr.lower()
def test_template_render_strict_vs_lenient_mode(self):
"""Test strict vs lenient mode behavior."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Template with missing variable
template_file = temp_path / "template.md"
template_file.write_text("Hello {{name}}, missing: {{missing}}")
# Data missing the 'missing' variable
data_file = temp_path / "data.json"
data_file.write_text(json.dumps({"name": "Alice"}))
# Test strict mode (should fail)
result_strict = subprocess.run(
['markitect', 'template-render', str(template_file), str(data_file), '--strict'],
capture_output=True,
text=True
)
assert result_strict.returncode != 0
# Test lenient mode (should succeed)
result_lenient = subprocess.run(
['markitect', 'template-render', str(template_file), str(data_file), '--lenient'],
capture_output=True,
text=True
)
assert result_lenient.returncode == 0
output = result_lenient.stdout
assert "Hello Alice" in output
assert "{{missing}}" in output # Placeholder preserved
class TestCLIRegressionPrevention:
"""Tests specifically designed to catch common CLI regression patterns."""
def test_import_paths_valid(self):
"""Test that all CLI module imports work correctly.
This catches issues like the domain module import that broke CLI access.
"""
# Try to import the CLI module directly
try:
import markitect.cli
# Should not raise ImportError or ModuleNotFoundError
except (ImportError, ModuleNotFoundError) as e:
pytest.fail(f"CLI module import failed: {e}")
def test_cli_entry_point_configuration(self):
"""Test that the CLI entry point is properly configured."""
# Check that the entry point script exists and is executable
import shutil
markitect_path = shutil.which('markitect')
assert markitect_path is not None, "markitect command not found in PATH"
assert os.access(markitect_path, os.X_OK), "markitect command is not executable"
def test_no_runtime_import_errors(self):
"""Test that basic CLI commands don't have runtime import errors."""
# Test a few key commands to ensure no import errors at runtime
commands_to_test = [
['markitect', '--version'], # Should show version or error gracefully
['markitect', 'list', '--help'], # Core command help
['markitect', 'template-render', '--help'], # New template command help
]
for cmd in commands_to_test:
result = subprocess.run(cmd, capture_output=True, text=True)
# Even if command fails, it shouldn't be due to import errors
assert "ModuleNotFoundError" not in result.stderr
assert "ImportError" not in result.stderr
assert "No module named" not in result.stderr
def test_template_engine_availability(self):
"""Test that template engine is properly available to CLI."""
# Create minimal test to ensure template engine can be imported by CLI
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
template_file = temp_path / "minimal.md"
template_file.write_text("test")
data_file = temp_path / "minimal.json"
data_file.write_text("{}")
# This should not fail with import errors
result = subprocess.run(
['markitect', 'template-render', str(template_file), str(data_file)],
capture_output=True,
text=True
)
# Should succeed or fail gracefully, but not with import errors
assert "ImportError" not in result.stderr
assert "ModuleNotFoundError" not in result.stderr
assert "Template engine not available" not in result.stderr
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -0,0 +1,504 @@
"""
Test for Issue #65: Template Engine Foundation - Integration Tests
This test module validates complete template engine integration scenarios
for business document generation, implementing TDD8 Cycle 3.
Tests focus on:
- Real business document template rendering (invoices, reports)
- End-to-end template processing workflows
- Performance with realistic data volumes
- Integration with MarkdownMatters metadata structure
"""
import pytest
from typing import Dict, Any
class TestTemplateEngineIntegration:
"""Test suite for template engine integration scenarios."""
def setup_method(self):
"""Set up test environment for each test."""
try:
from markitect.template.engine import TemplateEngine
self.engine = TemplateEngine()
except ImportError:
self.engine = None
def test_render_complete_invoice_template(self):
"""Test rendering a complete business invoice template.
Reference: Issue #65 - Template Engine Foundation
TDD Phase: Integration test for business use case
"""
# Arrange - Complete invoice template from examples/invoice_template.md
invoice_template = """---
title: "Invoice {{invoice_number}}"
date: "{{date}}"
due_date: "{{due_date}}"
customer_id: "{{customer.id}}"
---
{{company.name}}
{{company.address}}
{{company.city}}, {{company.state}} {{company.zip}}
{{company.email}} | {{company.phone}}
# Invoice {{invoice_number}}
**Bill To:**
{{customer.name}}
{{customer.address}}
{{customer.city}}, {{customer.state}} {{customer.zip}}
**Invoice Date:** {{date}}
**Due Date:** {{due_date}}
**Customer ID:** {{customer.id}}
## Summary
**Subtotal:** {{subtotal}}
**Tax ({{tax_rate}}%):** {{tax_amount}}
**Total:** {{total}} {{currency}}
## Payment Information
Please remit payment to {{company.name}} within {{payment_terms}} days.
---
{{!contentmatter}}
invoice_number: "{{invoice_number}}"
customer: "{{customer.name}}"
total_amount: {{total}}
currency: "{{currency}}"
status: "generated"
{{!/contentmatter}}
"""
# Test data representing realistic invoice data
invoice_data = {
"invoice_number": "INV-2025-001",
"date": "2025-01-15",
"due_date": "2025-02-14",
"company": {
"name": "MarkiTect Solutions",
"address": "123 Business Park",
"city": "Tech City",
"state": "CA",
"zip": "90210",
"email": "billing@markitect.com",
"phone": "(555) 123-4567"
},
"customer": {
"id": "CUST-001",
"name": "Acme Corporation",
"address": "456 Industry Blvd",
"city": "Enterprise",
"state": "NY",
"zip": "10001"
},
"subtotal": 1500.00,
"tax_rate": 8.5,
"tax_amount": 127.50,
"total": 1627.50,
"currency": "USD",
"payment_terms": "30"
}
# Act & Assert
if self.engine is None:
pytest.skip("TemplateEngine not implemented yet - TDD integration phase")
result = self.engine.render(invoice_template, invoice_data)
# Verify critical invoice elements are rendered correctly
assert "Invoice INV-2025-001" in result
assert "MarkiTect Solutions" in result
assert "Acme Corporation" in result
assert "123 Business Park" in result
assert "Tech City, CA 90210" in result
assert "Invoice Date:** 2025-01-15" in result
assert "Due Date:** 2025-02-14" in result
assert "Customer ID:** CUST-001" in result
assert "**Total:** 1627.5 USD" in result
assert "Tax (8.5%):** 127.5" in result
assert "within 30 days" in result
# Verify frontmatter is rendered
assert 'title: "Invoice INV-2025-001"' in result
assert 'customer_id: "CUST-001"' in result
# Verify contentmatter placeholders are rendered
assert 'invoice_number: "INV-2025-001"' in result
assert 'customer: "Acme Corporation"' in result
assert 'total_amount: 1627.5' in result
def test_render_business_report_template(self):
"""Test rendering a business report template with calculations.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
report_template = """---
title: "{{report_type}} Report - {{period}}"
generated: "{{generated_date}}"
department: "{{department.name}}"
---
# {{report_type}} Report
**Period:** {{period}}
**Department:** {{department.name}}
**Generated:** {{generated_date}}
## Summary
- Total Revenue: {{metrics.revenue}} {{currency}}
- Total Expenses: {{metrics.expenses}} {{currency}}
- Net Profit: {{metrics.profit}} {{currency}}
- Profit Margin: {{metrics.profit_margin}}%
## Department Performance
**Manager:** {{department.manager}}
**Team Size:** {{department.team_size}}
**Budget Utilization:** {{department.budget_utilization}}%
Contact: {{department.contact.email}}
"""
report_data = {
"report_type": "Monthly Financial",
"period": "January 2025",
"generated_date": "2025-02-01",
"currency": "USD",
"department": {
"name": "Sales",
"manager": "Sarah Johnson",
"team_size": 12,
"budget_utilization": 85.5,
"contact": {
"email": "sales@company.com"
}
},
"metrics": {
"revenue": 125000.00,
"expenses": 87500.00,
"profit": 37500.00,
"profit_margin": 30.0
}
}
# Act & Assert
if self.engine is None:
pytest.skip("TemplateEngine not implemented yet")
result = self.engine.render(report_template, report_data)
# Verify report structure and data
assert "Monthly Financial Report" in result
assert "Period:** January 2025" in result
assert "Department:** Sales" in result
assert "Total Revenue: 125000.0 USD" in result
assert "Net Profit: 37500.0 USD" in result
assert "Profit Margin: 30.0%" in result
assert "Manager:** Sarah Johnson" in result
assert "Budget Utilization:** 85.5%" in result
assert "sales@company.com" in result
def test_error_handling_missing_nested_data(self):
"""Test comprehensive error handling with detailed context.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
template = "Customer: {{customer.profile.details.name}}, Order: {{order.items.first.description}}"
incomplete_data = {
"customer": {
"profile": {
# Missing 'details' key
}
},
"order": {
# Missing 'items' key
"id": "ORD-001"
}
}
# Act & Assert
if self.engine is None:
pytest.skip("TemplateEngine not implemented yet")
# Test strict mode error with context
with pytest.raises(Exception) as exc_info:
self.engine.render(template, incomplete_data, strict=True)
error_message = str(exc_info.value)
# Should provide helpful context about what was available
assert ("details" in error_message.lower() or
"customer.profile.details.name" in error_message)
# Test lenient mode preserves placeholders
result = self.engine.render(template, incomplete_data, strict=False)
assert "{{customer.profile.details.name}}" in result
assert "{{order.items.first.description}}" in result
def test_performance_large_business_document(self):
"""Test performance with realistic large business document.
Reference: Issue #65 - Performance Requirements
"""
# Arrange - Large template with many variables
large_template = """# Annual Report {{year}}
## Executive Summary
Company: {{company.name}}
CEO: {{company.ceo}}
Revenue: {{financials.revenue}} {{currency}}
## Department Reports
"""
# Add many department sections
for i in range(50):
dept_prefix = f"departments.dept_{i}"
large_template += f"""
### Department {{{{{dept_prefix}.name}}}}
Manager: {{{{{dept_prefix}.manager}}}}
Budget: {{{{{dept_prefix}.budget}}}} {{{{currency}}}}
Team Size: {{{{{dept_prefix}.team_size}}}}
"""
# Generate corresponding data
departments_data = {}
for i in range(50):
departments_data[f"dept_{i}"] = {
"name": f"Department {i+1}",
"manager": f"Manager {i+1}",
"budget": (i+1) * 10000,
"team_size": (i % 20) + 5
}
large_data = {
"year": "2025",
"currency": "USD",
"company": {
"name": "Enterprise Corp",
"ceo": "John CEO"
},
"financials": {
"revenue": 50000000
},
"departments": departments_data
}
# Act & Assert
if self.engine is None:
pytest.skip("TemplateEngine not implemented yet")
import time
start_time = time.time()
result = self.engine.render(large_template, large_data)
render_time = time.time() - start_time
# Performance requirement: <100ms for large documents
assert render_time < 0.1
# Verify content was rendered
assert "Annual Report 2025" in result
assert "Enterprise Corp" in result
assert "50000000 USD" in result
assert "Department 1" in result
assert "Department 50" in result
def test_markdown_structure_preservation(self):
"""Test that complex markdown structure is preserved during rendering.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange - Complex markdown with various elements
complex_template = """---
title: "{{document.title}}"
author: "{{document.author}}"
---
# {{document.title}}
## Table of Contents
- [Introduction](#introduction)
- [Analysis](#analysis-{{section.id}})
- [Conclusion](#conclusion)
## Introduction
Welcome to **{{document.title}}** by *{{document.author}}*.
> This document provides {{description.type}} analysis for {{client.name}}.
### Code Example
```python
def process_{{operation.name}}():
return "{{operation.result}}"
```
## Analysis {{section.id}}
| Metric | Value | Target |
|--------|-------|--------|
| {{metrics.primary.name}} | {{metrics.primary.value}} | {{metrics.primary.target}} |
| {{metrics.secondary.name}} | {{metrics.secondary.value}} | {{metrics.secondary.target}} |
### Subsection
1. First point about {{analysis.point1}}
2. Second point about {{analysis.point2}}
3. Third point with [link]({{external.url}})
---
*Generated on {{generation.date}} by {{generation.system}}*
"""
template_data = {
"document": {
"title": "Business Analysis Report",
"author": "Analytics Team"
},
"description": {
"type": "comprehensive"
},
"client": {
"name": "Global Enterprises"
},
"section": {
"id": "Q1-2025"
},
"operation": {
"name": "quarterly_analysis",
"result": "success"
},
"metrics": {
"primary": {
"name": "Revenue",
"value": "$125K",
"target": "$120K"
},
"secondary": {
"name": "Growth",
"value": "12%",
"target": "10%"
}
},
"analysis": {
"point1": "market expansion",
"point2": "customer acquisition"
},
"external": {
"url": "https://example.com/data"
},
"generation": {
"date": "2025-01-15",
"system": "MarkiTect"
}
}
# Act & Assert
if self.engine is None:
pytest.skip("TemplateEngine not implemented yet")
result = self.engine.render(complex_template, template_data)
# Verify markdown structure preservation
assert "---" in result # Frontmatter
assert "# Business Analysis Report" in result # H1
assert "## Table of Contents" in result # H2
assert "- [Introduction](#introduction)" in result # List
assert "> This document provides" in result # Blockquote
assert "```python" in result # Code block
assert "def process_quarterly_analysis():" in result # Rendered in code
assert "| Revenue | $125K | $120K |" in result # Table
assert "1. First point about market expansion" in result # Numbered list
assert "[link](https://example.com/data)" in result # Link
assert "*Generated on 2025-01-15 by MarkiTect*" in result # Emphasis
# Verify frontmatter variables were rendered
assert 'title: "Business Analysis Report"' in result
assert 'author: "Analytics Team"' in result
class TestTemplateEngineWorkflows:
"""Test complete template processing workflows."""
def setup_method(self):
"""Set up test environment."""
try:
from markitect.template.engine import TemplateEngine
from markitect.template.parser import TemplateParser
self.engine = TemplateEngine()
self.parser = TemplateParser()
except ImportError:
self.engine = None
self.parser = None
def test_template_validation_workflow(self):
"""Test complete template validation before rendering workflow.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
template_with_errors = "Valid: {{name}}, Invalid: {{broken, Incomplete: {missing}"
valid_template = "Hello {{name}}, welcome to {{company}}!"
test_data = {"name": "Alice", "company": "MarkiTect"}
# Act & Assert
if self.engine is None or self.parser is None:
pytest.skip("Template components not implemented yet")
# Test validation of problematic template
errors = self.engine.validate_template(template_with_errors)
assert len(errors) > 0
# Test validation of good template
errors = self.engine.validate_template(valid_template)
assert len(errors) == 0
# Test rendering after validation
result = self.engine.render(valid_template, test_data)
assert result == "Hello Alice, welcome to MarkiTect!"
def test_data_completeness_analysis_workflow(self):
"""Test data completeness analysis before rendering.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
template = "Invoice {{invoice_number}} for {{customer.name}} - Total: {{total}} {{currency}}"
complete_data = {
"invoice_number": "INV-001",
"customer": {"name": "Acme Corp"},
"total": 1500.00,
"currency": "USD"
}
incomplete_data = {
"invoice_number": "INV-001",
"customer": {"name": "Acme Corp"}
# Missing 'total' and 'currency'
}
# Act & Assert
if self.engine is None:
pytest.skip("TemplateEngine not implemented yet")
# Test with complete data
completeness = self.engine.check_data_completeness(template, complete_data)
assert completeness['completeness'] == 1.0
assert len(completeness['missing']) == 0
assert len(completeness['available']) == 4
# Test with incomplete data
completeness = self.engine.check_data_completeness(template, incomplete_data)
assert completeness['completeness'] < 1.0
assert 'total' in completeness['missing']
assert 'currency' in completeness['missing']
assert 'invoice_number' in completeness['available']
assert 'customer.name' in completeness['available']
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -0,0 +1,222 @@
"""
Test for Issue #65: Template Engine Foundation - Template Variable Parser
This test module validates the core template variable parsing functionality
for the MarkiTect template engine, implementing TDD8 Cycle 1.
Tests focus on:
- Basic variable parsing from template strings
- Nested object variable extraction
- Markdown structure preservation during parsing
"""
import pytest
from typing import List, Set
class TestTemplateVariableParser:
"""Test suite for template variable parsing functionality."""
def setup_method(self):
"""Set up test environment for each test."""
# Import the template parser (will be implemented)
# For now, this will fail - following TDD RED phase
try:
from markitect.template.parser import TemplateParser
self.parser = TemplateParser()
except ImportError:
# Expected to fail initially - TDD RED phase
self.parser = None
def test_parse_simple_variables(self):
"""Test basic variable parsing from template strings.
Reference: Issue #65 - Template Engine Foundation
TDD Phase: RED (test should fail initially)
"""
# Arrange
template_text = "Hello {{name}}, welcome to {{company}}!"
expected_variables = {"name", "company"}
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
variables = self.parser.extract_variables(template_text)
assert isinstance(variables, (list, set))
assert set(variables) == expected_variables
def test_parse_nested_variables(self):
"""Test nested object variable parsing 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}}"
expected_variables = {"customer.name", "customer.contact.email"}
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
variables = self.parser.extract_variables(template_text)
assert set(variables) == expected_variables
def test_parse_markdown_with_variables(self):
"""Test variable parsing from markdown content while preserving structure.
Reference: Issue #65 - Template Engine Foundation
TDD Phase: RED (test should fail initially)
"""
# Arrange
template_text = """---
title: "Invoice {{invoice_number}}"
customer: "{{customer.name}}"
---
# Invoice {{invoice_number}}
**Bill To**: {{customer.name}}
**Email**: {{customer.email}}
**Total**: {{total}} {{currency}}
## Line Items
| Description | Amount |
|-------------|--------|
| Service | {{service.amount}} |
"""
expected_variables = {
"invoice_number",
"customer.name",
"customer.email",
"total",
"currency",
"service.amount"
}
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
variables = self.parser.extract_variables(template_text)
assert set(variables) == expected_variables
def test_parse_duplicate_variables(self):
"""Test that duplicate variables are handled correctly.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
template_text = "{{name}} says hello to {{name}} and {{company}}"
expected_variables = {"name", "company"}
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
variables = self.parser.extract_variables(template_text)
assert set(variables) == expected_variables
def test_parse_empty_template(self):
"""Test parsing template with no variables.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
template_text = "This is a regular markdown document with no variables."
expected_variables = set()
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
variables = self.parser.extract_variables(template_text)
assert set(variables) == expected_variables
def test_parse_malformed_variables(self):
"""Test handling of malformed variable syntax.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
template_text = "Valid: {{name}}, Invalid: {{broken, Incomplete: {missing}"
expected_variables = {"name"} # Only valid variables should be extracted
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
variables = self.parser.extract_variables(template_text)
assert set(variables) == expected_variables
def test_parse_nested_braces(self):
"""Test handling of nested braces and complex syntax.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
template_text = "Code: {{code.value}} and JSON: {\"key\": \"{{data.field}}\"}"
expected_variables = {"code.value", "data.field"}
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
variables = self.parser.extract_variables(template_text)
assert set(variables) == expected_variables
class TestTemplateParserEdgeCases:
"""Test edge cases and error conditions for template parser."""
def setup_method(self):
"""Set up test environment."""
try:
from markitect.template.parser import TemplateParser
self.parser = TemplateParser()
except ImportError:
self.parser = None
def test_parse_extremely_long_template(self):
"""Test parsing performance with large templates.
Reference: Issue #65 - Performance Requirements
"""
# Arrange
# Create a large template with many variables
variables = [f"{{{{field_{i}}}}}" for i in range(1000)]
template_text = " ".join(variables)
expected_count = 1000
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
import time
start_time = time.time()
variables = self.parser.extract_variables(template_text)
parse_time = time.time() - start_time
assert len(variables) == expected_count
assert parse_time < 0.1 # Should parse large templates quickly
def test_parse_unicode_variables(self):
"""Test parsing templates with unicode content.
Reference: Issue #65 - Template Engine Foundation
"""
# Arrange
template_text = "Grüße {{name}}, café {{café.price}} €"
expected_variables = {"name", "café.price"}
# Act & Assert
if self.parser is None:
pytest.skip("TemplateParser not implemented yet - TDD RED phase")
variables = self.parser.extract_variables(template_text)
assert set(variables) == expected_variables
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View 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'])