feat: Add template system with testing showcase
Implements template-based project creation allowing teams to start with
professional example code instead of empty projects.
Features:
- Two templates: 'basic' (minimal) and 'testing' (45-test showcase)
- Template variable substitution ({{PROJECT_NAME}}, etc.)
- Template validation with helpful error messages
- `weevil new --list-templates` command
- Templates embedded in binary at compile time
Testing template includes:
- 3 complete subsystems (MotorCycler, WallApproach, TurnController)
- Hardware abstraction layer with mock implementations
- 45 comprehensive tests (unit, integration, system)
- Professional documentation (DESIGN_AND_TEST_PLAN.md, etc.)
Usage:
weevil new my-robot # basic template
weevil new my-robot --template testing # testing showcase
weevil new --list-templates # show available templates
This enables FTC teams to learn from working code and best practices
rather than starting from scratch.
All 62 tests passing.
This commit is contained in:
309
tests/template_tests.rs
Normal file
309
tests/template_tests.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
// Import the template system
|
||||
use weevil::templates::{TemplateManager, TemplateContext};
|
||||
|
||||
/// Helper to create a test template context
|
||||
fn test_context(project_name: &str) -> TemplateContext {
|
||||
TemplateContext {
|
||||
project_name: project_name.to_string(),
|
||||
package_name: project_name.to_lowercase().replace("-", "").replace("_", ""),
|
||||
creation_date: "2026-02-02T12:00:00Z".to_string(),
|
||||
weevil_version: "1.1.0-test".to_string(),
|
||||
template_name: "basic".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_manager_creation() {
|
||||
let mgr = TemplateManager::new();
|
||||
assert!(mgr.is_ok(), "TemplateManager should be created successfully");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_exists() {
|
||||
let mgr = TemplateManager::new().unwrap();
|
||||
|
||||
assert!(mgr.template_exists("basic"), "basic template should exist");
|
||||
assert!(mgr.template_exists("testing"), "testing template should exist");
|
||||
assert!(!mgr.template_exists("nonexistent"), "nonexistent template should not exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_templates() {
|
||||
let mgr = TemplateManager::new().unwrap();
|
||||
let templates = mgr.list_templates();
|
||||
|
||||
assert_eq!(templates.len(), 2, "Should have exactly 2 templates");
|
||||
assert!(templates.iter().any(|t| t.contains("basic")), "Should list basic template");
|
||||
assert!(templates.iter().any(|t| t.contains("testing")), "Should list testing template");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_template_extraction() -> Result<()> {
|
||||
let mgr = TemplateManager::new()?;
|
||||
let temp_dir = TempDir::new()?;
|
||||
let project_dir = temp_dir.path().join("test-robot");
|
||||
fs::create_dir(&project_dir)?;
|
||||
|
||||
let context = test_context("test-robot");
|
||||
let file_count = mgr.extract_template("basic", &project_dir, &context)?;
|
||||
|
||||
assert!(file_count > 0, "Should extract at least one file from basic template");
|
||||
|
||||
// Verify key files exist (basic template has minimal files)
|
||||
assert!(project_dir.join(".gitignore").exists(), ".gitignore should exist");
|
||||
assert!(project_dir.join("README.md").exists(), "README.md should exist (processed from .template)");
|
||||
assert!(project_dir.join("settings.gradle").exists(), "settings.gradle should exist");
|
||||
|
||||
// Note: .weevil.toml and build.gradle are created by ProjectBuilder, not template
|
||||
|
||||
// Verify OpMode exists
|
||||
let opmode_path = project_dir.join("src/main/java/robot/opmodes/BasicOpMode.java");
|
||||
assert!(opmode_path.exists(), "BasicOpMode.java should exist");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_testing_template_extraction() -> Result<()> {
|
||||
let mgr = TemplateManager::new()?;
|
||||
let temp_dir = TempDir::new()?;
|
||||
let project_dir = temp_dir.path().join("test-showcase");
|
||||
fs::create_dir(&project_dir)?;
|
||||
|
||||
let mut context = test_context("test-showcase");
|
||||
context.template_name = "testing".to_string();
|
||||
|
||||
let file_count = mgr.extract_template("testing", &project_dir, &context)?;
|
||||
|
||||
assert!(file_count > 20, "Testing template should have 20+ files, got {}", file_count);
|
||||
|
||||
// Verify documentation files
|
||||
assert!(project_dir.join("README.md").exists(), "README.md should exist");
|
||||
assert!(project_dir.join("DESIGN_AND_TEST_PLAN.md").exists(), "DESIGN_AND_TEST_PLAN.md should exist");
|
||||
assert!(project_dir.join("TESTING_GUIDE.md").exists(), "TESTING_GUIDE.md should exist");
|
||||
|
||||
// Verify subsystems
|
||||
assert!(project_dir.join("src/main/java/robot/subsystems/MotorCycler.java").exists(), "MotorCycler.java should exist");
|
||||
assert!(project_dir.join("src/main/java/robot/subsystems/WallApproach.java").exists(), "WallApproach.java should exist");
|
||||
assert!(project_dir.join("src/main/java/robot/subsystems/TurnController.java").exists(), "TurnController.java should exist");
|
||||
|
||||
// Verify hardware interfaces and implementations
|
||||
assert!(project_dir.join("src/main/java/robot/hardware/MotorController.java").exists(), "MotorController interface should exist");
|
||||
assert!(project_dir.join("src/main/java/robot/hardware/FtcMotorController.java").exists(), "FtcMotorController should exist");
|
||||
assert!(project_dir.join("src/main/java/robot/hardware/DistanceSensor.java").exists(), "DistanceSensor interface should exist");
|
||||
assert!(project_dir.join("src/main/java/robot/hardware/FtcDistanceSensor.java").exists(), "FtcDistanceSensor should exist");
|
||||
|
||||
// Verify test files
|
||||
assert!(project_dir.join("src/test/java/robot/subsystems/MotorCyclerTest.java").exists(), "MotorCyclerTest.java should exist");
|
||||
assert!(project_dir.join("src/test/java/robot/subsystems/WallApproachTest.java").exists(), "WallApproachTest.java should exist");
|
||||
assert!(project_dir.join("src/test/java/robot/subsystems/TurnControllerTest.java").exists(), "TurnControllerTest.java should exist");
|
||||
|
||||
// Verify mock implementations
|
||||
assert!(project_dir.join("src/test/java/robot/hardware/MockMotorController.java").exists(), "MockMotorController should exist");
|
||||
assert!(project_dir.join("src/test/java/robot/hardware/MockDistanceSensor.java").exists(), "MockDistanceSensor should exist");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_variable_substitution() -> Result<()> {
|
||||
let mgr = TemplateManager::new()?;
|
||||
let temp_dir = TempDir::new()?;
|
||||
let project_dir = temp_dir.path().join("my-test-robot");
|
||||
fs::create_dir(&project_dir)?;
|
||||
|
||||
let context = test_context("my-test-robot");
|
||||
mgr.extract_template("basic", &project_dir, &context)?;
|
||||
|
||||
// Check README.md for variable substitution
|
||||
let readme_path = project_dir.join("README.md");
|
||||
let readme_content = fs::read_to_string(readme_path)?;
|
||||
|
||||
assert!(readme_content.contains("my-test-robot"), "README should contain project name");
|
||||
assert!(readme_content.contains("1.1.0-test"), "README should contain weevil version");
|
||||
assert!(!readme_content.contains("{{PROJECT_NAME}}"), "README should not contain template variable");
|
||||
assert!(!readme_content.contains("{{WEEVIL_VERSION}}"), "README should not contain template variable");
|
||||
|
||||
// Check BasicOpMode.java for variable substitution
|
||||
let opmode_path = project_dir.join("src/main/java/robot/opmodes/BasicOpMode.java");
|
||||
let opmode_content = fs::read_to_string(opmode_path)?;
|
||||
|
||||
assert!(opmode_content.contains("my-test-robot"), "BasicOpMode should contain project name");
|
||||
assert!(!opmode_content.contains("{{PROJECT_NAME}}"), "BasicOpMode should not contain template variable");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_template_extraction() {
|
||||
let mgr = TemplateManager::new().unwrap();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let project_dir = temp_dir.path().join("test-robot");
|
||||
fs::create_dir(&project_dir).unwrap();
|
||||
|
||||
let context = test_context("test-robot");
|
||||
let result = mgr.extract_template("nonexistent", &project_dir, &context);
|
||||
|
||||
assert!(result.is_err(), "Should fail for nonexistent template");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_package_name_sanitization() {
|
||||
// Test that the helper creates correct package names
|
||||
let context1 = test_context("my-robot");
|
||||
assert_eq!(context1.package_name, "myrobot", "Hyphens should be removed");
|
||||
|
||||
let context2 = test_context("team_1234_bot");
|
||||
assert_eq!(context2.package_name, "team1234bot", "Underscores should be removed");
|
||||
|
||||
let context3 = test_context("My-Cool_Bot");
|
||||
assert_eq!(context3.package_name, "mycoolbot", "Mixed case and separators should be handled");
|
||||
}
|
||||
|
||||
/// Integration test: Create a project with testing template and run gradle tests
|
||||
/// This is marked with #[ignore] by default since it requires:
|
||||
/// - Java installed
|
||||
/// - Network access (first time to download gradle wrapper)
|
||||
/// - Takes ~1-2 minutes to run
|
||||
///
|
||||
/// Run with: cargo test test_testing_template_gradle_build -- --ignored --nocapture
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_testing_template_gradle_build() -> Result<()> {
|
||||
println!("Testing complete gradle build and test execution...");
|
||||
|
||||
let mgr = TemplateManager::new()?;
|
||||
let temp_dir = TempDir::new()?;
|
||||
let project_dir = temp_dir.path().join("gradle-test-robot");
|
||||
fs::create_dir(&project_dir)?;
|
||||
|
||||
// Extract testing template
|
||||
let mut context = test_context("gradle-test-robot");
|
||||
context.template_name = "testing".to_string();
|
||||
|
||||
let file_count = mgr.extract_template("testing", &project_dir, &context)?;
|
||||
println!("Extracted {} files from testing template", file_count);
|
||||
|
||||
// Check if gradlew exists (should be in testing template)
|
||||
let gradlew = if cfg!(windows) {
|
||||
project_dir.join("gradlew.bat")
|
||||
} else {
|
||||
project_dir.join("gradlew")
|
||||
};
|
||||
|
||||
if !gradlew.exists() {
|
||||
println!("WARNING: gradlew not found in template, skipping gradle test");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Make gradlew executable on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&gradlew)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&gradlew, perms)?;
|
||||
}
|
||||
|
||||
println!("Running gradle test...");
|
||||
|
||||
// Run gradlew test
|
||||
let output = Command::new(&gradlew)
|
||||
.arg("test")
|
||||
.current_dir(&project_dir)
|
||||
.output()?;
|
||||
|
||||
println!("=== Gradle Output ===");
|
||||
println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
|
||||
if !output.status.success() {
|
||||
println!("=== Gradle Errors ===");
|
||||
println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
panic!("Gradle tests failed with status: {}", output.status);
|
||||
}
|
||||
|
||||
// Verify test output mentions 45 tests
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Look for test success indicators
|
||||
let has_success = stdout.contains("BUILD SUCCESSFUL") ||
|
||||
stdout.contains("45 tests") ||
|
||||
stdout.to_lowercase().contains("tests passed");
|
||||
|
||||
assert!(has_success, "Gradle test output should indicate success");
|
||||
|
||||
println!("✓ All 45 tests passed!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that basic template creates a valid directory structure
|
||||
#[test]
|
||||
fn test_basic_template_directory_structure() -> Result<()> {
|
||||
let mgr = TemplateManager::new()?;
|
||||
let temp_dir = TempDir::new()?;
|
||||
let project_dir = temp_dir.path().join("structure-test");
|
||||
fs::create_dir(&project_dir)?;
|
||||
|
||||
let context = test_context("structure-test");
|
||||
mgr.extract_template("basic", &project_dir, &context)?;
|
||||
|
||||
// Verify directory structure
|
||||
assert!(project_dir.join("src").is_dir(), "src directory should exist");
|
||||
assert!(project_dir.join("src/main").is_dir(), "src/main directory should exist");
|
||||
assert!(project_dir.join("src/main/java").is_dir(), "src/main/java directory should exist");
|
||||
assert!(project_dir.join("src/main/java/robot").is_dir(), "src/main/java/robot directory should exist");
|
||||
assert!(project_dir.join("src/main/java/robot/opmodes").is_dir(), "opmodes directory should exist");
|
||||
assert!(project_dir.join("src/test/java/robot").is_dir(), "test directory should exist");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that .gitignore is not named ".gitignore.template"
|
||||
#[test]
|
||||
fn test_gitignore_naming() -> Result<()> {
|
||||
let mgr = TemplateManager::new()?;
|
||||
let temp_dir = TempDir::new()?;
|
||||
let project_dir = temp_dir.path().join("gitignore-test");
|
||||
fs::create_dir(&project_dir)?;
|
||||
|
||||
let context = test_context("gitignore-test");
|
||||
mgr.extract_template("basic", &project_dir, &context)?;
|
||||
|
||||
assert!(project_dir.join(".gitignore").exists(), ".gitignore should exist");
|
||||
assert!(!project_dir.join(".gitignore.template").exists(), ".gitignore.template should NOT exist");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that template extraction doesn't fail with unusual project names
|
||||
#[test]
|
||||
fn test_unusual_project_names() -> Result<()> {
|
||||
let mgr = TemplateManager::new()?;
|
||||
|
||||
let test_names = vec![
|
||||
"robot-2024",
|
||||
"team_1234",
|
||||
"FTC_Bot",
|
||||
"my-awesome-bot",
|
||||
];
|
||||
|
||||
for name in test_names {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let project_dir = temp_dir.path().join(name);
|
||||
fs::create_dir(&project_dir)?;
|
||||
|
||||
let context = test_context(name);
|
||||
let result = mgr.extract_template("basic", &project_dir, &context);
|
||||
|
||||
assert!(result.is_ok(), "Should handle project name: {}", name);
|
||||
assert!(project_dir.join("README.md").exists(), "README should exist for {}", name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user