use tempfile::TempDir; use std::fs; use std::path::Path; use anvil::templates::{TemplateManager, TemplateContext}; use anvil::project::config::ProjectConfig; // ============================================================================ // Template extraction tests // ============================================================================ #[test] fn test_basic_template_extracts_all_expected_files() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "test_proj".to_string(), anvil_version: "1.0.0".to_string(), }; let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); assert!(count >= 10, "Expected at least 10 files, got {}", count); } #[test] fn test_template_creates_sketch_directory() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let sketch_dir = tmp.path().join("blink"); assert!(sketch_dir.is_dir(), "Sketch directory should exist"); let ino_file = sketch_dir.join("blink.ino"); assert!(ino_file.exists(), "Sketch .ino file should exist"); // Verify the .ino content has correct includes let content = fs::read_to_string(&ino_file).unwrap(); assert!( content.contains("blink_app.h"), ".ino should include project-specific app header" ); assert!( content.contains("hal_arduino.h"), ".ino should include hal_arduino.h" ); } #[test] fn test_template_creates_hal_files() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "sensor".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); assert!( tmp.path().join("lib/hal/hal.h").exists(), "hal.h should exist" ); assert!( tmp.path().join("lib/hal/hal_arduino.h").exists(), "hal_arduino.h should exist" ); // Verify hal.h defines the abstract Hal class let hal_content = fs::read_to_string(tmp.path().join("lib/hal/hal.h")).unwrap(); assert!( hal_content.contains("class Hal"), "hal.h should define class Hal" ); assert!( hal_content.contains("virtual void pinMode"), "hal.h should declare pinMode" ); } #[test] fn test_template_creates_app_header() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "my_sensor".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let app_path = tmp.path().join("lib/app/my_sensor_app.h"); assert!(app_path.exists(), "App header should exist with project name"); let content = fs::read_to_string(&app_path).unwrap(); assert!( content.contains("#include "), "App header should include hal.h" ); assert!( content.contains("class BlinkApp"), "App header should define BlinkApp class" ); } #[test] fn test_template_creates_test_infrastructure() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); assert!( tmp.path().join("test/CMakeLists.txt").exists(), "CMakeLists.txt should exist" ); assert!( tmp.path().join("test/test_unit.cpp").exists(), "test_unit.cpp should exist" ); assert!( tmp.path().join("test/mocks/mock_hal.h").exists(), "mock_hal.h should exist" ); assert!( tmp.path().join("test/mocks/sim_hal.h").exists(), "sim_hal.h should exist" ); assert!( tmp.path().join("test/run_tests.sh").exists(), "run_tests.sh should exist" ); assert!( tmp.path().join("test/run_tests.bat").exists(), "run_tests.bat should exist" ); } #[test] fn test_template_test_file_references_correct_app() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "motor_ctrl".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let test_content = fs::read_to_string( tmp.path().join("test/test_unit.cpp") ).unwrap(); assert!( test_content.contains("motor_ctrl_app.h"), "Test file should include project-specific app header" ); } #[test] fn test_template_cmake_references_correct_project() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "my_bot".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let cmake = fs::read_to_string( tmp.path().join("test/CMakeLists.txt") ).unwrap(); assert!( cmake.contains("my_bot"), "CMakeLists.txt should contain project name" ); } #[test] fn test_template_creates_dot_files() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); assert!( tmp.path().join(".gitignore").exists(), ".gitignore should be created from _dot_ prefix" ); assert!( tmp.path().join(".editorconfig").exists(), ".editorconfig should be created" ); assert!( tmp.path().join(".clang-format").exists(), ".clang-format should be created" ); assert!( tmp.path().join(".vscode/settings.json").exists(), ".vscode/settings.json should be created" ); } #[test] fn test_template_creates_readme() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let readme = tmp.path().join("README.md"); assert!(readme.exists(), "README.md should exist"); let content = fs::read_to_string(&readme).unwrap(); assert!(content.contains("blink"), "README should contain project name"); } // ============================================================================ // .anvil.toml config tests // ============================================================================ #[test] fn test_template_creates_valid_config() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); // Should be loadable by ProjectConfig let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.project.name, "blink"); assert_eq!(config.build.fqbn, "arduino:avr:uno"); assert_eq!(config.monitor.baud, 115200); assert!(config.build.extra_flags.contains(&"-Werror".to_string())); } #[test] fn test_config_roundtrip() { let tmp = TempDir::new().unwrap(); let config = ProjectConfig::new("roundtrip_test"); config.save(tmp.path()).unwrap(); let loaded = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(loaded.project.name, "roundtrip_test"); assert_eq!(loaded.build.fqbn, config.build.fqbn); assert_eq!(loaded.monitor.baud, config.monitor.baud); assert_eq!(loaded.build.include_dirs, config.build.include_dirs); } #[test] fn test_config_find_project_root_walks_up() { let tmp = TempDir::new().unwrap(); let config = ProjectConfig::new("walk_test"); config.save(tmp.path()).unwrap(); // Create nested subdirectory let deep = tmp.path().join("sketch").join("src").join("deep"); fs::create_dir_all(&deep).unwrap(); let found = ProjectConfig::find_project_root(&deep).unwrap(); assert_eq!(found, tmp.path().canonicalize().unwrap()); } #[test] fn test_config_resolve_include_flags() { let tmp = TempDir::new().unwrap(); fs::create_dir_all(tmp.path().join("lib/hal")).unwrap(); fs::create_dir_all(tmp.path().join("lib/app")).unwrap(); let config = ProjectConfig::new("flags_test"); let flags = config.resolve_include_flags(tmp.path()); assert_eq!(flags.len(), 2); assert!(flags[0].starts_with("-I")); assert!(flags[0].ends_with("lib/hal") || flags[0].ends_with("lib\\hal")); } #[test] fn test_config_skips_nonexistent_include_dirs() { let tmp = TempDir::new().unwrap(); // Don't create the directories let config = ProjectConfig::new("missing_dirs"); let flags = config.resolve_include_flags(tmp.path()); assert_eq!(flags.len(), 0, "Should skip non-existent directories"); } // ============================================================================ // Full project creation test (end-to-end in temp dir) // ============================================================================ #[test] fn test_full_project_structure() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "full_test".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); // Verify the complete expected file tree let expected_files = vec![ ".anvil.toml", ".gitignore", ".editorconfig", ".clang-format", ".vscode/settings.json", "README.md", "full_test/full_test.ino", "lib/hal/hal.h", "lib/hal/hal_arduino.h", "lib/app/full_test_app.h", "test/CMakeLists.txt", "test/test_unit.cpp", "test/run_tests.sh", "test/run_tests.bat", "test/mocks/mock_hal.h", "test/mocks/sim_hal.h", ]; for f in &expected_files { let p = tmp.path().join(f); assert!( p.exists(), "Expected file missing: {} (checked {})", f, p.display() ); } } #[test] fn test_no_unicode_in_template_output() { // Eric's rule: only ASCII characters let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "ascii_test".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); // Check all generated text files for non-ASCII check_ascii_recursive(tmp.path()); } fn check_ascii_recursive(dir: &Path) { for entry in fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); let path = entry.path(); if path.is_dir() { check_ascii_recursive(&path); } else { // Only check text files let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); if matches!(ext, "h" | "cpp" | "ino" | "txt" | "sh" | "bat" | "json" | "toml" | "md") { let content = fs::read_to_string(&path).unwrap(); for (line_num, line) in content.lines().enumerate() { for (col, ch) in line.chars().enumerate() { assert!( ch.is_ascii(), "Non-ASCII character '{}' (U+{:04X}) at {}:{}:{} ", ch, ch as u32, path.display(), line_num + 1, col + 1 ); } } } } } } // ============================================================================ // Error case tests // ============================================================================ #[test] fn test_unknown_template_fails() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "test".to_string(), anvil_version: "1.0.0".to_string(), }; let result = TemplateManager::extract("nonexistent", tmp.path(), &ctx); assert!(result.is_err()); } #[test] fn test_load_config_from_nonproject_fails() { let tmp = TempDir::new().unwrap(); let result = ProjectConfig::load(tmp.path()); assert!(result.is_err()); }