use anvil::commands; use anvil::ignore::{self, AnvilIgnore}; use anvil::library; use anvil::project::config::ProjectConfig; use anvil::templates::{TemplateManager, TemplateContext}; use std::fs; use tempfile::TempDir; // ========================================================================= // Template registry // ========================================================================= #[test] fn test_list_templates_includes_weather() { let templates = TemplateManager::list_templates(); assert!( templates.iter().any(|t| t.name == "weather"), "Weather template should be listed" ); } #[test] fn test_weather_template_exists() { assert!(TemplateManager::template_exists("weather")); } #[test] fn test_weather_is_not_default() { let templates = TemplateManager::list_templates(); let weather = templates.iter().find(|t| t.name == "weather").unwrap(); assert!(!weather.is_default); } #[test] fn test_weather_lists_tmp36_library() { let templates = TemplateManager::list_templates(); let weather = templates.iter().find(|t| t.name == "weather").unwrap(); assert!(weather.libraries.contains(&"tmp36".to_string())); } #[test] fn test_weather_lists_analog_capability() { let templates = TemplateManager::list_templates(); let weather = templates.iter().find(|t| t.name == "weather").unwrap(); assert!(weather.board_capabilities.contains(&"analog".to_string())); } // ========================================================================= // Composed metadata // ========================================================================= #[test] fn test_weather_composed_meta_exists() { let meta = TemplateManager::composed_meta("weather"); assert!(meta.is_some(), "Weather should have composed metadata"); } #[test] fn test_weather_composed_meta_base_is_basic() { let meta = TemplateManager::composed_meta("weather").unwrap(); assert_eq!(meta.base, "basic"); } #[test] fn test_weather_composed_meta_requires_tmp36() { let meta = TemplateManager::composed_meta("weather").unwrap(); assert!(meta.libraries.contains(&"tmp36".to_string())); } #[test] fn test_weather_composed_meta_has_pin_defaults() { let meta = TemplateManager::composed_meta("weather").unwrap(); assert!(!meta.pins.is_empty()); } #[test] fn test_weather_pins_for_uno() { let meta = TemplateManager::composed_meta("weather").unwrap(); let pins = meta.pins_for_board("uno"); assert_eq!(pins.len(), 1); assert_eq!(pins[0].name, "tmp36_data"); assert_eq!(pins[0].pin, "A0"); assert_eq!(pins[0].mode, "analog"); } #[test] fn test_weather_pins_fallback_to_default() { let meta = TemplateManager::composed_meta("weather").unwrap(); // "micro" is not explicitly listed, should fall back to "default" let pins = meta.pins_for_board("micro"); assert!(!pins.is_empty()); assert!(pins.iter().any(|p| p.name == "tmp36_data")); } #[test] fn test_basic_has_no_composed_meta() { assert!(TemplateManager::composed_meta("basic").is_none()); } // ========================================================================= // Template extraction -- file overlay // ========================================================================= fn extract_weather(name: &str) -> TempDir { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: name.to_string(), anvil_version: "1.0.0".to_string(), board_name: "uno".to_string(), fqbn: "arduino:avr:uno".to_string(), baud: 115200, }; TemplateManager::extract("weather", tmp.path(), &ctx).unwrap(); tmp } #[test] fn test_weather_has_basic_scaffold() { let tmp = extract_weather("wx"); assert!(tmp.path().join(".anvil.toml").exists()); assert!(tmp.path().join("build.sh").exists()); assert!(tmp.path().join("build.bat").exists()); assert!(tmp.path().join("upload.sh").exists()); assert!(tmp.path().join("monitor.sh").exists()); assert!(tmp.path().join(".gitignore").exists()); assert!(tmp.path().join("lib").join("hal").join("hal.h").exists()); } #[test] fn test_weather_has_weather_app() { let tmp = extract_weather("wx"); let app_path = tmp.path().join("lib").join("app").join("wx_app.h"); assert!(app_path.exists(), "Weather app header should exist"); let content = fs::read_to_string(&app_path).unwrap(); assert!(content.contains("WeatherApp")); assert!(content.contains("TempSensor")); assert!(content.contains("readCelsius")); assert!(content.contains("readFahrenheit")); assert!(content.contains("READ_INTERVAL_MS")); } #[test] fn test_weather_app_replaces_basic_blink() { let tmp = extract_weather("wx"); let app_path = tmp.path().join("lib").join("app").join("wx_app.h"); let content = fs::read_to_string(&app_path).unwrap(); // Should NOT contain basic template's BlinkApp assert!( !content.contains("BlinkApp"), "Weather app should replace basic blink, not include it" ); } #[test] fn test_weather_has_weather_sketch() { let tmp = extract_weather("wx"); let ino_path = tmp.path().join("wx").join("wx.ino"); assert!(ino_path.exists()); let content = fs::read_to_string(&ino_path).unwrap(); assert!(content.contains("Tmp36Analog")); assert!(content.contains("WeatherApp")); assert!(content.contains("hal_arduino.h")); assert!(content.contains("tmp36_analog.h")); } #[test] fn test_weather_sketch_replaces_basic_sketch() { let tmp = extract_weather("wx"); let ino_path = tmp.path().join("wx").join("wx.ino"); let content = fs::read_to_string(&ino_path).unwrap(); assert!( !content.contains("BlinkApp"), "Weather sketch should replace basic, not extend it" ); } #[test] fn test_weather_has_managed_example_tests() { let tmp = extract_weather("wx"); let test_path = tmp.path().join("test").join("test_weather.cpp"); assert!(test_path.exists(), "Managed test_weather.cpp should exist"); let content = fs::read_to_string(&test_path).unwrap(); assert!(content.contains("Tmp36Mock")); assert!(content.contains("WeatherUnitTest")); assert!(content.contains("Tmp36Sim")); assert!(content.contains("WeatherSystemTest")); assert!(content.contains("wx_app.h")); assert!(content.contains("MANAGED BY ANVIL")); } #[test] fn test_weather_has_student_unit_starter() { let tmp = extract_weather("wx"); let test_path = tmp.path().join("test").join("test_unit.cpp"); assert!(test_path.exists()); let content = fs::read_to_string(&test_path).unwrap(); // Should be a minimal starter, not the full weather tests assert!(content.contains("Your unit tests go here")); assert!(!content.contains("WeatherUnitTest")); } #[test] fn test_weather_has_student_system_starter() { let tmp = extract_weather("wx"); let test_path = tmp.path().join("test").join("test_system.cpp"); assert!(test_path.exists()); let content = fs::read_to_string(&test_path).unwrap(); assert!(content.contains("Your system tests go here")); assert!(!content.contains("WeatherSystemTest")); } #[test] fn test_weather_no_template_toml_in_output() { let tmp = extract_weather("wx"); assert!( !tmp.path().join("template.toml").exists(), "template.toml should be stripped from output" ); } #[test] fn test_weather_preserves_cmake() { let tmp = extract_weather("wx"); let cmake = tmp.path().join("test").join("CMakeLists.txt"); assert!(cmake.exists()); let content = fs::read_to_string(&cmake).unwrap(); assert!(content.contains("wx")); } #[test] fn test_weather_variable_substitution() { let tmp = extract_weather("my_weather"); // App header should use project name let app_path = tmp .path() .join("lib") .join("app") .join("my_weather_app.h"); assert!(app_path.exists()); // Sketch should use project name let ino_path = tmp .path() .join("my_weather") .join("my_weather.ino"); assert!(ino_path.exists()); // Config should use project name let config = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap(); assert!(config.contains("my_weather")); } #[test] fn test_weather_all_files_ascii() { let tmp = extract_weather("wx"); let mut checked = 0; for entry in walkdir(tmp.path()) { let content = fs::read(&entry).unwrap(); for (i, &byte) in content.iter().enumerate() { assert!( byte < 128, "Non-ASCII byte {} at offset {} in {}", byte, i, entry.display() ); } checked += 1; } assert!(checked > 0, "Should have checked some files"); } /// Walk all files in a directory recursively. fn walkdir(dir: &std::path::Path) -> Vec { let mut files = vec![]; if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { files.extend(walkdir(&path)); } else { files.push(path); } } } files } // ========================================================================= // Library installation via install_library // ========================================================================= #[test] fn test_install_library_adds_tmp36_to_project() { let tmp = extract_weather("wx"); let written = commands::lib::install_library("tmp36", tmp.path()).unwrap(); assert!(!written.is_empty(), "Should write at least one file"); // Verify driver directory created let driver_dir = tmp.path().join("lib").join("drivers").join("tmp36"); assert!(driver_dir.exists()); assert!(driver_dir.join("tmp36.h").exists()); assert!(driver_dir.join("tmp36_analog.h").exists()); assert!(driver_dir.join("tmp36_mock.h").exists()); assert!(driver_dir.join("tmp36_sim.h").exists()); // Verify config updated let config = ProjectConfig::load(tmp.path()).unwrap(); assert!(config.libraries.contains_key("tmp36")); assert!(config.build.include_dirs.contains( &"lib/drivers/tmp36".to_string() )); } #[test] fn test_install_library_idempotent() { let tmp = extract_weather("wx"); let first = commands::lib::install_library("tmp36", tmp.path()).unwrap(); assert!(!first.is_empty()); // Second call should be a no-op let second = commands::lib::install_library("tmp36", tmp.path()).unwrap(); assert!(second.is_empty(), "Second install should skip"); } // ========================================================================= // Pin assignment via install_pin_assignment // ========================================================================= #[test] fn test_install_pin_assignment_creates_assignment() { let tmp = extract_weather("wx"); // Need library installed first (for the include_dirs, not strictly // needed for pin assignment but mirrors real flow) commands::lib::install_library("tmp36", tmp.path()).unwrap(); commands::pin::install_pin_assignment( "tmp36_data", "A0", "analog", "uno", tmp.path(), ) .unwrap(); // Verify config has the assignment let config = ProjectConfig::load(tmp.path()).unwrap(); let pins = config.pins.get("uno").expect("Should have uno pins"); assert!(pins.assignments.contains_key("tmp36_data")); let a = &pins.assignments["tmp36_data"]; assert_eq!(a.pin, 14); // A0 = pin 14 on Uno assert_eq!(a.mode, "analog"); } #[test] fn test_install_pin_assignment_rejects_bad_pin() { let tmp = extract_weather("wx"); let result = commands::pin::install_pin_assignment( "tmp36_data", "Z99", "analog", "uno", tmp.path(), ); assert!(result.is_err()); } #[test] fn test_install_pin_assignment_rejects_bad_mode() { let tmp = extract_weather("wx"); let result = commands::pin::install_pin_assignment( "tmp36_data", "A0", "bogus", "uno", tmp.path(), ); assert!(result.is_err()); } // ========================================================================= // Full composed template flow (extract + install libs + assign pins) // ========================================================================= #[test] fn test_weather_full_flow() { let tmp = extract_weather("my_wx"); // Install libraries (as the new command would) let meta = TemplateManager::composed_meta("weather").unwrap(); for lib_name in &meta.libraries { commands::lib::install_library(lib_name, tmp.path()).unwrap(); } // Assign pins (as the new command would) let pin_defaults = meta.pins_for_board("uno"); for pin_def in &pin_defaults { commands::pin::install_pin_assignment( &pin_def.name, &pin_def.pin, &pin_def.mode, "uno", tmp.path(), ) .unwrap(); } // Verify everything is in place let config = ProjectConfig::load(tmp.path()).unwrap(); // Library installed assert!(config.libraries.contains_key("tmp36")); assert!(config.build.include_dirs.contains( &"lib/drivers/tmp36".to_string() )); // Pin assigned let pins = config.pins.get("uno").expect("Should have uno pins"); assert!(pins.assignments.contains_key("tmp36_data")); // Driver files present assert!(tmp .path() .join("lib") .join("drivers") .join("tmp36") .join("tmp36.h") .exists()); // App code present (weather-specific, not blink) let app_content = fs::read_to_string( tmp.path().join("lib").join("app").join("my_wx_app.h"), ) .unwrap(); assert!(app_content.contains("WeatherApp")); assert!(!app_content.contains("BlinkApp")); // Test files present (weather-specific) let weather_content = fs::read_to_string( tmp.path().join("test").join("test_weather.cpp"), ) .unwrap(); assert!(weather_content.contains("Tmp36Mock")); assert!(weather_content.contains("Tmp36Sim")); // Sketch wires everything together let ino_content = fs::read_to_string( tmp.path().join("my_wx").join("my_wx.ino"), ) .unwrap(); assert!(ino_content.contains("Tmp36Analog")); assert!(ino_content.contains("WeatherApp")); } #[test] fn test_weather_flow_with_mega() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "mega_wx".to_string(), anvil_version: "1.0.0".to_string(), board_name: "mega".to_string(), fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), baud: 115200, }; TemplateManager::extract("weather", tmp.path(), &ctx).unwrap(); let meta = TemplateManager::composed_meta("weather").unwrap(); for lib_name in &meta.libraries { commands::lib::install_library(lib_name, tmp.path()).unwrap(); } let pin_defaults = meta.pins_for_board("mega"); for pin_def in &pin_defaults { commands::pin::install_pin_assignment( &pin_def.name, &pin_def.pin, &pin_def.mode, "mega", tmp.path(), ) .unwrap(); } let config = ProjectConfig::load(tmp.path()).unwrap(); assert!(config.libraries.contains_key("tmp36")); let pins = config.pins.get("mega").expect("Should have mega pins"); assert!(pins.assignments.contains_key("tmp36_data")); } // ========================================================================= // Audit integration -- after template creation, audit should be clean // ========================================================================= #[test] fn test_weather_audit_clean_after_full_setup() { let tmp = extract_weather("wx"); let meta = TemplateManager::composed_meta("weather").unwrap(); for lib_name in &meta.libraries { commands::lib::install_library(lib_name, tmp.path()).unwrap(); } for pin_def in meta.pins_for_board("uno") { commands::pin::install_pin_assignment( &pin_def.name, &pin_def.pin, &pin_def.mode, "uno", tmp.path(), ) .unwrap(); } // Load config and check that library pins are all assigned let config = ProjectConfig::load(tmp.path()).unwrap(); let lib_meta = library::find_library("tmp36").unwrap(); let pins = config.pins.get("uno").unwrap(); let assigned_names: Vec = pins.assignments.keys().cloned().collect(); let unassigned = library::unassigned_pins(&lib_meta, &assigned_names); assert!( unassigned.is_empty(), "All library pins should be assigned after full setup, \ but these are missing: {:?}", unassigned ); } // ========================================================================= // API compatibility -- template C++ must match library headers // ========================================================================= /// Extract public method names from a C++ header (simple regex-free scan). /// Looks for lines like: "void methodName(" or "float methodName(" or /// "ClassName(" (constructors). fn extract_public_methods(header: &str) -> Vec { let mut methods = Vec::new(); for line in header.lines() { let trimmed = line.trim(); // Skip comments, preprocessor, blank if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") || trimmed.starts_with('#') || trimmed.is_empty() { continue; } // Look for "word(" patterns that are method declarations // e.g. "void setBaseTemperature(float celsius)" // e.g. "Tmp36Sim(float base_temp = 22.0f, float noise = 0.5f)" if let Some(paren_pos) = trimmed.find('(') { let before = trimmed[..paren_pos].trim(); // Last word before the paren is the method name if let Some(name) = before.split_whitespace().last() { // Skip class/struct declarations if name == "class" || name == "struct" || name == "if" || name == "for" || name == "while" || name == "override" { continue; } methods.push(name.to_string()); } } } methods } /// Count the number of default parameters in a constructor signature. /// e.g. "Tmp36Sim(float base_temp = 22.0f, float noise = 0.5f)" /// has 2 params, both with defaults. fn count_constructor_params(header: &str, class_name: &str) -> (usize, usize) { for line in header.lines() { let trimmed = line.trim(); if !trimmed.contains(&format!("{}(", class_name)) { continue; } if let Some(start) = trimmed.find('(') { if let Some(end) = trimmed.find(')') { let params_str = &trimmed[start + 1..end]; if params_str.trim().is_empty() { return (0, 0); } let params: Vec<&str> = params_str.split(',').collect(); let total = params.len(); let with_defaults = params.iter().filter(|p| p.contains('=')).count(); return (total, with_defaults); } } } (0, 0) } #[test] fn test_template_system_tests_use_valid_sim_api() { let tmp = extract_weather("wx"); commands::lib::install_library("tmp36", tmp.path()).unwrap(); let sim_header = fs::read_to_string( tmp.path() .join("lib") .join("drivers") .join("tmp36") .join("tmp36_sim.h"), ) .unwrap(); let test_source = fs::read_to_string( tmp.path().join("test").join("test_weather.cpp"), ) .unwrap(); let methods = extract_public_methods(&sim_header); // Verify test code only calls methods that exist in the header // Check for common method-call patterns: "sensor.methodName(" // or "exact_sensor.methodName(" etc. for line in test_source.lines() { let trimmed = line.trim(); if trimmed.starts_with("//") || trimmed.is_empty() { continue; } // Find "identifier.methodName(" patterns if let Some(dot_pos) = trimmed.find('.') { let after_dot = &trimmed[dot_pos + 1..]; if let Some(paren_pos) = after_dot.find('(') { let method_name = after_dot[..paren_pos].trim(); // Only check methods on sensor/sim-like objects let before_dot = trimmed[..dot_pos].trim(); let before_dot = before_dot .split_whitespace() .last() .unwrap_or(before_dot); if before_dot.contains("sensor") || before_dot.contains("sim") { assert!( methods.contains(&method_name.to_string()), "test_weather.cpp calls '{}.{}()' but '{}' \ is not in tmp36_sim.h.\n \ Available methods: {:?}", before_dot, method_name, method_name, methods ); } } } } } #[test] fn test_template_unit_tests_use_valid_mock_api() { let tmp = extract_weather("wx"); commands::lib::install_library("tmp36", tmp.path()).unwrap(); let mock_header = fs::read_to_string( tmp.path() .join("lib") .join("drivers") .join("tmp36") .join("tmp36_mock.h"), ) .unwrap(); let test_source = fs::read_to_string( tmp.path().join("test").join("test_weather.cpp"), ) .unwrap(); let methods = extract_public_methods(&mock_header); for line in test_source.lines() { let trimmed = line.trim(); if trimmed.starts_with("//") || trimmed.is_empty() { continue; } if let Some(dot_pos) = trimmed.find('.') { let after_dot = &trimmed[dot_pos + 1..]; if let Some(paren_pos) = after_dot.find('(') { let method_name = after_dot[..paren_pos].trim(); let before_dot = trimmed[..dot_pos].trim(); let before_dot = before_dot .split_whitespace() .last() .unwrap_or(before_dot); if before_dot.contains("sensor") { assert!( methods.contains(&method_name.to_string()), "test_weather.cpp calls '{}.{}()' but '{}' \ is not in tmp36_mock.h.\n \ Available methods: {:?}", before_dot, method_name, method_name, methods ); } } } } } #[test] fn test_template_sim_constructor_arg_count() { let tmp = extract_weather("wx"); commands::lib::install_library("tmp36", tmp.path()).unwrap(); let sim_header = fs::read_to_string( tmp.path() .join("lib") .join("drivers") .join("tmp36") .join("tmp36_sim.h"), ) .unwrap(); let (total_params, default_params) = count_constructor_params(&sim_header, "Tmp36Sim"); let test_source = fs::read_to_string( tmp.path().join("test").join("test_weather.cpp"), ) .unwrap(); // Check every "Tmp36Sim(" call in test code let min_args = total_params - default_params; for (line_num, line) in test_source.lines().enumerate() { let trimmed = line.trim(); if trimmed.starts_with("//") { continue; } // Find "Tmp36Sim(" constructor calls (not #include or class decl) if let Some(pos) = trimmed.find("Tmp36Sim(") { // Skip if it's a forward decl or class line if trimmed.contains("class ") || trimmed.contains("#include") { continue; } let after = &trimmed[pos + 9..]; // after "Tmp36Sim(" if let Some(close) = after.find(')') { let args_str = &after[..close]; let arg_count = if args_str.trim().is_empty() { 0 } else { args_str.split(',').count() }; assert!( arg_count >= min_args && arg_count <= total_params, "test_weather.cpp line {}: Tmp36Sim() called with \ {} args, but constructor accepts {}-{} args.\n \ Line: {}", line_num + 1, arg_count, min_args, total_params, trimmed ); } } } } // ========================================================================= // .anvilignore -- generated defaults and behavior // ========================================================================= /// Helper: full weather project setup (extract + set template + libs + pins + .anvilignore) fn setup_weather_project(name: &str) -> TempDir { let tmp = extract_weather(name); // Set template field in config (as new.rs does) let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.project.template = "weather".to_string(); config.save(tmp.path()).unwrap(); // Generate .anvilignore ignore::generate_default(tmp.path(), "weather").unwrap(); // Install library and assign pins let meta = TemplateManager::composed_meta("weather").unwrap(); for lib_name in &meta.libraries { commands::lib::install_library(lib_name, tmp.path()).unwrap(); } for pin_def in meta.pins_for_board("uno") { commands::pin::install_pin_assignment( &pin_def.name, &pin_def.pin, &pin_def.mode, "uno", tmp.path(), ) .unwrap(); } tmp } #[test] fn test_anvilignore_generated_on_project_creation() { let tmp = setup_weather_project("wx"); assert!( tmp.path().join(".anvilignore").exists(), ".anvilignore should be created" ); } #[test] fn test_anvilignore_protects_student_test_files() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!( ignore.is_ignored("test/test_unit.cpp"), "test_unit.cpp should be protected" ); assert!( ignore.is_ignored("test/test_system.cpp"), "test_system.cpp should be protected" ); } #[test] fn test_anvilignore_protects_app_code() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!( ignore.is_ignored("lib/app/wx_app.h"), "App code should be protected by lib/app/* pattern" ); } #[test] fn test_anvilignore_protects_config() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!( ignore.is_ignored(".anvil.toml"), ".anvil.toml should be protected" ); } #[test] fn test_anvilignore_does_not_protect_managed_scripts() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!( !ignore.is_ignored("build.sh"), "build.sh should NOT be protected" ); assert!( !ignore.is_ignored("test/run_tests.sh"), "run_tests.sh should NOT be protected" ); assert!( !ignore.is_ignored("test/mocks/mock_hal.h"), "mock_hal.h should NOT be protected" ); } #[test] fn test_anvilignore_does_not_protect_managed_template_test() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!( !ignore.is_ignored("test/test_weather.cpp"), "test_weather.cpp is managed by Anvil, should NOT be protected" ); } #[test] fn test_anvilignore_does_not_protect_library_headers() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!( !ignore.is_ignored("lib/drivers/tmp36/tmp36.h"), "Driver headers are managed, should NOT be protected" ); } // ========================================================================= // .anvilignore -- add/remove patterns // ========================================================================= #[test] fn test_add_ignore_pattern() { let tmp = setup_weather_project("wx"); ignore::add_pattern(tmp.path(), "test/test_custom.cpp").unwrap(); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!(ignore.is_ignored("test/test_custom.cpp")); } #[test] fn test_add_ignore_pattern_no_duplicate() { let tmp = setup_weather_project("wx"); ignore::add_pattern(tmp.path(), "test/test_unit.cpp").unwrap(); let content = fs::read_to_string(tmp.path().join(".anvilignore")).unwrap(); // test_unit.cpp appears in the default, adding it again should not duplicate let count = content .lines() .filter(|l| l.trim() == "test/test_unit.cpp") .count(); assert_eq!(count, 1); } #[test] fn test_add_ignore_wildcard_pattern() { let tmp = setup_weather_project("wx"); ignore::add_pattern(tmp.path(), "test/my_*.cpp").unwrap(); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!(ignore.is_ignored("test/my_custom_test.cpp")); assert!(ignore.is_ignored("test/my_helper.cpp")); assert!(!ignore.is_ignored("test/test_weather.cpp")); } #[test] fn test_remove_ignore_pattern() { let tmp = setup_weather_project("wx"); // test_unit.cpp is in defaults let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!(ignore.is_ignored("test/test_unit.cpp")); // Remove it let removed = ignore::remove_pattern(tmp.path(), "test/test_unit.cpp").unwrap(); assert!(removed); // Verify it's gone let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!(!ignore.is_ignored("test/test_unit.cpp")); } #[test] fn test_remove_nonexistent_pattern_returns_false() { let tmp = setup_weather_project("wx"); let removed = ignore::remove_pattern(tmp.path(), "nonexistent.cpp").unwrap(); assert!(!removed); } // ========================================================================= // .anvilignore -- matching_pattern reports which rule matched // ========================================================================= #[test] fn test_matching_pattern_reports_exact() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert_eq!( ignore.matching_pattern("test/test_unit.cpp"), Some("test/test_unit.cpp") ); } #[test] fn test_matching_pattern_reports_glob() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); // lib/app/* should match lib/app/wx_app.h let pattern = ignore.matching_pattern("lib/app/wx_app.h"); assert_eq!(pattern, Some("lib/app/*")); } #[test] fn test_matching_pattern_returns_none_for_unignored() { let tmp = setup_weather_project("wx"); let ignore = AnvilIgnore::load(tmp.path()).unwrap(); assert!(ignore.matching_pattern("build.sh").is_none()); } // ========================================================================= // Config tracks template name // ========================================================================= #[test] fn test_config_records_template_name() { let tmp = setup_weather_project("wx"); let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.project.template, "weather"); } #[test] fn test_config_default_template_is_basic() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "basic_proj".to_string(), anvil_version: "1.0.0".to_string(), board_name: "uno".to_string(), fqbn: "arduino:avr:uno".to_string(), baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.project.template, "basic"); } // ========================================================================= // Refresh respects .anvilignore // ========================================================================= #[test] fn test_refresh_does_not_overwrite_ignored_files() { let tmp = setup_weather_project("wx"); // Modify the student's test_unit.cpp (which is ignored) let test_unit = tmp.path().join("test").join("test_unit.cpp"); fs::write(&test_unit, "// my custom test code\n").unwrap(); // Run refresh --force commands::refresh::run_refresh( Some(tmp.path().to_str().unwrap()), true, None, ) .unwrap(); // Student's file should be untouched let content = fs::read_to_string(&test_unit).unwrap(); assert_eq!(content, "// my custom test code\n"); } #[test] fn test_refresh_updates_managed_template_test() { let tmp = setup_weather_project("wx"); // Tamper with managed test_weather.cpp let test_weather = tmp.path().join("test").join("test_weather.cpp"); let original = fs::read_to_string(&test_weather).unwrap(); fs::write(&test_weather, "// tampered\n").unwrap(); // Run refresh --force commands::refresh::run_refresh( Some(tmp.path().to_str().unwrap()), true, None, ) .unwrap(); // Managed file should be restored let content = fs::read_to_string(&test_weather).unwrap(); assert_ne!(content, "// tampered\n"); assert!(content.contains("WeatherUnitTest")); } #[test] fn test_refresh_force_file_overrides_ignore() { let tmp = setup_weather_project("wx"); // Modify ignored test_unit.cpp let test_unit = tmp.path().join("test").join("test_unit.cpp"); let original = fs::read_to_string(&test_unit).unwrap(); fs::write(&test_unit, "// i want this overwritten\n").unwrap(); // Run refresh --force --file test/test_unit.cpp commands::refresh::run_refresh( Some(tmp.path().to_str().unwrap()), true, Some("test/test_unit.cpp"), ) .unwrap(); // File should be restored to template version let content = fs::read_to_string(&test_unit).unwrap(); assert!( content.contains("Your unit tests go here"), "Should be restored to template starter" ); } #[test] fn test_refresh_updates_library_driver_headers() { let tmp = setup_weather_project("wx"); // Tamper with a driver header (managed) let header = tmp .path() .join("lib") .join("drivers") .join("tmp36") .join("tmp36.h"); fs::write(&header, "// tampered\n").unwrap(); // Run refresh --force commands::refresh::run_refresh( Some(tmp.path().to_str().unwrap()), true, None, ) .unwrap(); // Header should be restored let content = fs::read_to_string(&header).unwrap(); assert!(content.contains("TempSensor")); } #[test] fn test_refresh_freshly_created_project_is_up_to_date() { let tmp = setup_weather_project("wx"); // Refresh without --force should find nothing to do // (just verifying it doesn't error) commands::refresh::run_refresh( Some(tmp.path().to_str().unwrap()), false, None, ) .unwrap(); } #[test] fn test_anvilignore_all_files_ascii() { let tmp = setup_weather_project("wx"); let content = fs::read_to_string(tmp.path().join(".anvilignore")).unwrap(); for (i, byte) in content.bytes().enumerate() { assert!( byte < 128, "Non-ASCII byte {} at offset {} in .anvilignore", byte, i ); } }