Skip to main content

Testing & Validation

Ensure your templates are robust, reliable, and generate correct output with comprehensive testing strategies, validation techniques, and debugging approaches.

Template Validation

1. Syntax Validation

Create validation scripts to test your template syntax before deployment:

// validate_templates.rhai

// Test data for template validation
let test_data = #{
project_name: "test-project",
author: #{
name: "Test Author",
email: "test@example.com"
},
features: ["database", "auth"],
database_type: "postgresql"
};

// Set test variables
for key in test_data.keys() {
set_variable(key, test_data[key]);
}

// Validate template syntax
let template_files = [
"src/main.rs",
"src/lib.rs",
"Cargo.toml",
"README.md"
];

for file in template_files {
if !validate_template_syntax(file) {
log::error("Template syntax error in " + file);
return false;
}
log::info("✓ " + file + " syntax valid");
}

// Check for required variables
let required_vars = ["project_name", "author", "database_type"];
for var in required_vars {
if !has_variable(var) {
log::error("Missing required variable: " + var);
return false;
}
}

log::success("All templates validated successfully");

2. Variable Validation

Validate that all required variables are present and have valid values:

{# includes/validation.jinja #}
{% macro validate_required(var_name, var_value) %}
{% if not var_value %}
{% error var_name + " is required but not provided" %}
{% endif %}
{% endmacro %}

{% macro validate_choice(var_name, var_value, choices) %}
{% if var_value not in choices %}
{% error var_name + " must be one of: " + choices | join(", ") %}
{% endif %}
{% endmacro %}

{% macro validate_format(var_name, var_value, pattern, description) %}
{% if not var_value | regex_match(pattern) %}
{% error var_name + " must match format: " + description %}
{% endif %}
{% endmacro %}

Use validation macros in your templates:

{# src/main.rs #}
{% include "includes/validation.jinja" %}

{# Validate required variables #}
{{ validate_required("project_name", project_name) }}
{{ validate_required("author.name", author.name) }}

{# Validate choices #}
{{ validate_choice("database_type", database_type, ["postgresql", "mysql", "sqlite"]) }}

{# Validate format #}
{{ validate_format("project_name", project_name, "^[a-z][a-z0-9_]*$", "lowercase with underscores") }}

3. Output Validation

Create comprehensive tests for generated output:

#!/bin/bash
# test_template_output.sh

set -e # Exit on any error

echo "Starting template output validation..."

# Test different configurations
test_configs=(
"basic:project_name=test-basic,database_type=sqlite"
"advanced:project_name=test-advanced,database_type=postgresql,include_auth=true"
"minimal:project_name=test-minimal,features="
)

for config in "${test_configs[@]}"; do
IFS=':' read -r test_name params <<< "$config"
echo "Testing configuration: $test_name"

# Create test directory
test_dir="test-output-$test_name"
rm -rf "$test_dir"

# Generate project with specific configuration
archetect render . "$test_dir" --headless --overwrite \
$(echo "$params" | tr ',' '\n' | sed 's/^/-a /')

# Validate generated output
validate_output "$test_dir" "$test_name"
done

echo "✓ All template output tests passed!"

validate_output() {
local output_dir=$1
local config_name=$2

cd "$output_dir"

# Check basic file structure
check_file_exists "Cargo.toml"
check_file_exists "src/main.rs"
check_file_exists "README.md"

# Validate Rust syntax if cargo is available
if command -v cargo &> /dev/null; then
echo " Checking Rust syntax..."
if cargo check --quiet; then
echo " ✓ Generated Rust code is valid"
else
echo " ✗ Generated Rust code has syntax errors"
exit 1
fi
fi

# Validate file contents
validate_cargo_toml
validate_main_rs

# Configuration-specific validations
case "$config_name" in
"advanced")
check_file_exists "src/auth/mod.rs"
check_database_config "postgresql"
;;
"minimal")
check_file_not_exists "src/database/"
;;
esac

cd ..
}

check_file_exists() {
if [ -f "$1" ]; then
echo " ✓ $1 exists"
else
echo " ✗ $1 missing"
exit 1
fi
}

check_file_not_exists() {
if [ ! -e "$1" ]; then
echo " ✓ $1 correctly excluded"
else
echo " ✗ $1 should not exist"
exit 1
fi
}

validate_cargo_toml() {
if grep -q "^name = " Cargo.toml; then
echo " ✓ Cargo.toml has project name"
else
echo " ✗ Cargo.toml missing project name"
exit 1
fi
}

validate_main_rs() {
if grep -q "fn main" src/main.rs; then
echo " ✓ main.rs has main function"
else
echo " ✗ main.rs missing main function"
exit 1
fi
}

Debugging Templates

1. Template Debugging Techniques

Add debugging output to templates during development:

{# Enable debug mode via variable #}
{% set debug = debug | default(false) %}

{% if debug %}
{#
=== DEBUG INFORMATION ===
Template: {{ __template_name__ }}
Variables:
{% for key, value in __context__ %}
- {{ key }}: {{ value }}
{% endfor %}
=== END DEBUG ===
#}
{% endif %}

// Generated code starts here
use std::collections::HashMap;

{% if debug %}
// Debug: Processing {{ features | length }} features
{% endif %}

{% for feature in features %}
{% if debug %}
// Debug: Processing feature {{ loop.index }}/{{ features | length }}: {{ feature }}
{% endif %}
pub mod {{ feature | snake_case }};
{% endfor %}

2. Progressive Template Testing

Test templates incrementally during development:

{# src/lib.rs.debug #}
{# Start with minimal template #}
//! {{ project_name }} library

{% if step >= 1 %}
// Step 1: Basic exports
pub mod config;
{% endif %}

{% if step >= 2 %}
// Step 2: Add database support
{% if include_database %}
pub mod database;
{% endif %}
{% endif %}

{% if step >= 3 %}
// Step 3: Add features
{% for feature in features %}
pub mod {{ feature | snake_case }};
{% endfor %}
{% endif %}

Test with different step values:

# Test step by step
archetect render . test-step1 -a step=1
archetect render . test-step2 -a step=2
archetect render . test-step3 -a step=3

3. Error Handling and Recovery

Implement graceful error handling in templates:

{# src/config.rs #}
{% try %}
use {{ external_crate }}::Config;
{% catch %}
{# Fallback if external_crate is not available #}
use std::collections::HashMap as Config;
{% endtry %}

{% if database_type is defined %}
{% if database_type in ["postgresql", "mysql", "sqlite"] %}
use {{ database_type }}_driver as db;
{% else %}
{% error "Invalid database_type: " + database_type + ". Must be postgresql, mysql, or sqlite." %}
{% endif %}
{% else %}
// No database configuration - using in-memory storage
{% endif %}

Testing Frameworks and Tools

1. Automated Testing Pipeline

Create a comprehensive testing pipeline:

# .github/workflows/test-templates.yml
name: Test Templates

on: [push, pull_request]

jobs:
test-templates:
runs-on: ubuntu-latest
strategy:
matrix:
config:
- name: "minimal"
args: "project_name=test-minimal"
- name: "full-featured"
args: "project_name=test-full,database_type=postgresql,include_auth=true,include_api=true"
- name: "custom"
args: "project_name=test-custom,framework=axum,database_type=sqlite"

steps:
- uses: actions/checkout@v3

- name: Install Archetect
run: |
curl -sSL https://github.com/archetect/archetect/releases/latest/download/archetect-linux-x64 -o archetect
chmod +x archetect
sudo mv archetect /usr/local/bin/

- name: Generate project
run: |
archetect render . test-output-${{ matrix.config.name }} --headless \
$(echo "${{ matrix.config.args }}" | tr ',' '\n' | sed 's/^/-a /')

- name: Validate generated project
run: |
cd test-output-${{ matrix.config.name }}

# Check basic structure
test -f Cargo.toml
test -f src/main.rs
test -f README.md

# Install Rust if needed and validate syntax
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source ~/.cargo/env
cargo check

- name: Run integration tests
run: |
cd test-output-${{ matrix.config.name }}
if [ -f "tests/" ]; then
cargo test
fi

2. Template Linting

Create custom linting rules for templates:

#!/usr/bin/env python3
# lint_templates.py

import re
import sys
from pathlib import Path
from typing import List, Tuple

class TemplateLinter:
def __init__(self):
self.errors = []

def lint_file(self, file_path: Path) -> List[str]:
"""Lint a single template file."""
errors = []

try:
content = file_path.read_text(encoding='utf-8')
except UnicodeDecodeError:
# Skip binary files
return errors

lines = content.split('\n')

for line_num, line in enumerate(lines, 1):
errors.extend(self._check_line(file_path, line_num, line))

return errors

def _check_line(self, file_path: Path, line_num: int, line: str) -> List[str]:
"""Check a single line for issues."""
errors = []

# Check for unclosed template tags
if '{{' in line and '}}' not in line:
errors.append(f"{file_path}:{line_num}: Unclosed variable tag")

if '{%' in line and '%}' not in line:
errors.append(f"{file_path}:{line_num}: Unclosed template tag")

# Check for deprecated patterns
if re.search(r'\{\{\s*[^}]+\s*\|\s*safe\s*\}\}', line):
errors.append(f"{file_path}:{line_num}: Avoid 'safe' filter - ensure input is trusted")

# Check for common mistakes
if re.search(r'\{\{\s*[^}]+\s*==\s*[^}]+\s*\}\}', line):
errors.append(f"{file_path}:{line_num}: Use {% if %} instead of {{ }} for conditionals")

# Check for proper variable naming
var_matches = re.findall(r'\{\{\s*([a-zA-Z_][a-zA-Z0-9_\.]*)\s*(?:\|[^}]*)?\}\}', line)
for var in var_matches:
if var.startswith('_'):
errors.append(f"{file_path}:{line_num}: Variable '{var}' should not start with underscore")

return errors

def lint_directory(self, template_dir: Path) -> int:
"""Lint all template files in directory."""
total_errors = 0

for template_file in template_dir.rglob("*"):
if template_file.is_file() and not self._should_skip_file(template_file):
errors = self.lint_file(template_file)
if errors:
print(f"\nErrors in {template_file}:")
for error in errors:
print(f" {error}")
total_errors += len(errors)

return total_errors

def _should_skip_file(self, file_path: Path) -> bool:
"""Check if file should be skipped during linting."""
skip_patterns = [
'*.png', '*.jpg', '*.jpeg', '*.gif', '*.ico',
'*.zip', '*.tar', '*.gz', '*.bz2',
'.git/*', 'node_modules/*', 'target/*'
]

for pattern in skip_patterns:
if file_path.match(pattern):
return True

return False

def main():
linter = TemplateLinter()
template_dir = Path("content")

if not template_dir.exists():
print("Template directory 'content' not found")
sys.exit(1)

total_errors = linter.lint_directory(template_dir)

if total_errors == 0:
print("✓ All templates passed linting")
sys.exit(0)
else:
print(f"\n✗ Found {total_errors} errors")
sys.exit(1)

if __name__ == "__main__":
main()

3. Performance Testing

Test template rendering performance:

#!/usr/bin/env python3
# perf_test_templates.py

import time
import subprocess
import statistics
from pathlib import Path

def benchmark_rendering(iterations=5):
"""Benchmark template rendering performance."""
times = []

for i in range(iterations):
start_time = time.time()

# Run archetype generation
result = subprocess.run([
'archetect', 'render', '.', f'perf-test-{i}',
'--headless', '--overwrite',
'-a', 'project_name=perf-test',
'-a', 'database_type=postgresql',
'-a', 'include_auth=true'
], capture_output=True, text=True)

end_time = time.time()

if result.returncode != 0:
print(f"Error in iteration {i}: {result.stderr}")
continue

elapsed = end_time - start_time
times.append(elapsed)

# Cleanup
subprocess.run(['rm', '-rf', f'perf-test-{i}'], capture_output=True)

if times:
avg_time = statistics.mean(times)
min_time = min(times)
max_time = max(times)

print(f"Template rendering performance:")
print(f" Average: {avg_time:.2f}s")
print(f" Min: {min_time:.2f}s")
print(f" Max: {max_time:.2f}s")

if avg_time > 5.0:
print("⚠️ Template rendering is slow (>5s average)")
else:
print("✓ Template rendering performance is good")
else:
print("❌ No successful iterations")

if __name__ == "__main__":
benchmark_rendering()

This comprehensive testing and validation approach ensures your templates are reliable, maintainable, and produce correct output across different configurations and use cases.