use anvil::commands; use anvil::library; use anvil::project::config::ProjectConfig; use anvil::templates::{TemplateManager, TemplateContext}; use std::fs; use tempfile::TempDir; // ========================================================================== // Template listing and metadata // ========================================================================== #[test] fn test_list_templates_includes_button() { let templates = TemplateManager::list_templates(); assert!( templates.iter().any(|t| t.name == "button"), "Should list button template" ); } #[test] fn test_button_template_exists() { assert!(TemplateManager::template_exists("button")); } #[test] fn test_button_is_not_default() { let templates = TemplateManager::list_templates(); let btn = templates.iter().find(|t| t.name == "button").unwrap(); assert!(!btn.is_default); } #[test] fn test_button_lists_button_library() { let templates = TemplateManager::list_templates(); let btn = templates.iter().find(|t| t.name == "button").unwrap(); assert!(btn.libraries.contains(&"button".to_string())); } #[test] fn test_button_lists_digital_capability() { let templates = TemplateManager::list_templates(); let btn = templates.iter().find(|t| t.name == "button").unwrap(); assert!(btn.board_capabilities.contains(&"digital".to_string())); } // ========================================================================== // Composed metadata // ========================================================================== #[test] fn test_button_composed_meta_exists() { let meta = TemplateManager::composed_meta("button"); assert!(meta.is_some(), "Button should have composed metadata"); } #[test] fn test_button_composed_meta_base_is_basic() { let meta = TemplateManager::composed_meta("button").unwrap(); assert_eq!(meta.base, "basic"); } #[test] fn test_button_composed_meta_requires_button_lib() { let meta = TemplateManager::composed_meta("button").unwrap(); assert!(meta.libraries.contains(&"button".to_string())); } #[test] fn test_button_composed_meta_has_pin_defaults() { let meta = TemplateManager::composed_meta("button").unwrap(); assert!( !meta.pins.is_empty(), "Should have pin defaults" ); } #[test] fn test_button_pins_for_uno() { let meta = TemplateManager::composed_meta("button").unwrap(); let pins = meta.pins_for_board("uno"); assert_eq!(pins.len(), 1); assert_eq!(pins[0].name, "button_signal"); assert_eq!(pins[0].pin, "2"); assert_eq!(pins[0].mode, "input"); } #[test] fn test_button_pins_fallback_to_default() { let meta = TemplateManager::composed_meta("button").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 == "button_signal")); } // ========================================================================== // Template extraction // ========================================================================== fn extract_button(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("button", tmp.path(), &ctx).unwrap(); // Record template name in config, same as create_project does let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.project.template = "button".to_string(); config.save(tmp.path()).unwrap(); tmp } #[test] fn test_button_has_basic_scaffold() { let tmp = extract_button("btn"); // Basic scaffold from base template assert!(tmp.path().join("lib/hal/hal.h").exists()); assert!(tmp.path().join("lib/hal/hal_arduino.h").exists()); assert!(tmp.path().join("test/mocks/mock_hal.h").exists()); assert!(tmp.path().join("test/mocks/sim_hal.h").exists()); assert!(tmp.path().join("test/CMakeLists.txt").exists()); } #[test] fn test_button_has_button_app() { let tmp = extract_button("btn"); let app = tmp.path().join("lib/app/btn_app.h"); assert!(app.exists(), "Should have button app header"); let content = fs::read_to_string(&app).unwrap(); assert!(content.contains("class ButtonApp"), "Should define ButtonApp"); assert!(content.contains("isPressed"), "Should use button interface"); assert!(content.contains("was_pressed_"), "Should track previous state"); assert!(content.contains("press_count_"), "Should count presses"); assert!(content.contains("serialPrintln"), "Should print to serial"); } #[test] fn test_button_app_replaces_basic_blink() { let tmp = extract_button("btn"); let app = tmp.path().join("lib/app/btn_app.h"); let content = fs::read_to_string(&app).unwrap(); // Should NOT contain basic template's BlinkApp assert!(!content.contains("BlinkApp"), "Should not have BlinkApp"); assert!(content.contains("ButtonApp"), "Should have ButtonApp"); } #[test] fn test_button_has_button_sketch() { let tmp = extract_button("btn"); let sketch = tmp.path().join("btn/btn.ino"); assert!(sketch.exists(), "Sketch should exist"); let content = fs::read_to_string(&sketch).unwrap(); assert!(content.contains("button_digital.h"), "Should include button driver"); assert!(content.contains("ButtonDigital"), "Should create ButtonDigital"); assert!(content.contains("ButtonApp"), "Should create ButtonApp"); } #[test] fn test_button_sketch_replaces_basic_sketch() { let tmp = extract_button("btn"); let sketch = tmp.path().join("btn/btn.ino"); let content = fs::read_to_string(&sketch).unwrap(); assert!(!content.contains("BlinkApp"), "Should not reference BlinkApp"); assert!(content.contains("ButtonApp"), "Should reference ButtonApp"); } #[test] fn test_button_has_managed_example_tests() { let tmp = extract_button("btn"); let test_file = tmp.path().join("test/test_button_app.cpp"); assert!(test_file.exists(), "Should have managed test file"); let content = fs::read_to_string(&test_file).unwrap(); assert!(content.contains("MANAGED BY ANVIL"), "Should be marked as managed"); assert!(content.contains("ButtonUnitTest"), "Should have unit test fixture"); assert!(content.contains("ButtonSystemTest"), "Should have system test fixture"); } #[test] fn test_button_has_student_unit_starter() { let tmp = extract_button("btn"); let test_file = tmp.path().join("test/test_unit.cpp"); assert!(test_file.exists()); let content = fs::read_to_string(&test_file).unwrap(); assert!(content.contains("YOURS"), "Should be marked as student-owned"); assert!(content.contains("button_mock.h"), "Should include button mock"); assert!(content.contains("btn_app.h"), "Should include project app"); } #[test] fn test_button_has_student_system_starter() { let tmp = extract_button("btn"); let test_file = tmp.path().join("test/test_system.cpp"); assert!(test_file.exists()); let content = fs::read_to_string(&test_file).unwrap(); assert!(content.contains("YOURS"), "Should be marked as student-owned"); assert!(content.contains("button_sim.h"), "Should include button sim"); } #[test] fn test_button_no_template_toml_in_output() { let tmp = extract_button("btn"); assert!( !tmp.path().join("template.toml").exists(), "template.toml should not be extracted to output" ); } #[test] fn test_button_preserves_cmake() { let tmp = extract_button("btn"); let cmake = tmp.path().join("test/CMakeLists.txt"); assert!(cmake.exists(), "CMakeLists.txt should survive overlay"); } #[test] fn test_button_variable_substitution() { let tmp = extract_button("mybutton"); // Sketch should use project name let sketch = tmp.path().join("mybutton/mybutton.ino"); let content = fs::read_to_string(&sketch).unwrap(); assert!(content.contains("mybutton_app.h"), "Should substitute project name"); assert!(!content.contains("{{PROJECT_NAME}}"), "No unresolved placeholders"); // App header should use project name let app = tmp.path().join("lib/app/mybutton_app.h"); assert!(app.exists(), "App header should use project name"); // Test files should use project name let test_file = tmp.path().join("test/test_button_app.cpp"); let test_content = fs::read_to_string(&test_file).unwrap(); assert!(test_content.contains("mybutton_app.h"), "Test should include project app"); assert!(!test_content.contains("{{PROJECT_NAME}}"), "No unresolved placeholders"); } #[test] fn test_button_all_files_ascii() { let tmp = extract_button("btn"); for entry in walkdir(tmp.path()) { let content = fs::read_to_string(&entry).unwrap_or_default(); for (line_num, line) in content.lines().enumerate() { for (col, ch) in line.chars().enumerate() { assert!( ch.is_ascii(), "Non-ASCII in {} at {}:{}: U+{:04X}", entry.display(), line_num + 1, col + 1, ch as u32 ); } } } } // ========================================================================== // C++ API compatibility: test file references valid sensor methods // ========================================================================== #[test] fn test_template_button_tests_use_valid_button_api() { let tmp = extract_button("btn"); // Extract the button library so we can check method names library::extract_library("button", tmp.path()).unwrap(); // Read mock and sim headers to build the set of valid methods let mock_h = fs::read_to_string( tmp.path().join("lib/drivers/button/button_mock.h") ).unwrap(); let sim_h = fs::read_to_string( tmp.path().join("lib/drivers/button/button_sim.h") ).unwrap(); let test_file = fs::read_to_string( tmp.path().join("test/test_button_app.cpp") ).unwrap(); // Check that test calls methods that exist in mock or sim let expected_methods = ["isPressed", "setPressed", "press", "release", "setSeed", "setBounceReads"]; for method in &expected_methods { let in_mock = mock_h.contains(method); let in_sim = sim_h.contains(method); let in_test = test_file.contains(method); if in_test { assert!( in_mock || in_sim, "Test uses {}() but it is not in mock or sim headers", method ); } } } // ========================================================================== // Full flow: new project with button template // ========================================================================== #[test] fn test_button_full_flow() { let tmp = extract_button("btn_flow"); // Install the button library (as anvil new --template button would) let meta = library::find_library("button").unwrap(); library::extract_library("button", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.libraries.insert("button".to_string(), meta.version.clone()); let driver_include = format!("lib/drivers/{}", meta.name); if !config.build.include_dirs.contains(&driver_include) { config.build.include_dirs.push(driver_include); } config.save(tmp.path()).unwrap(); // Assign pin let dir_str = tmp.path().to_string_lossy().to_string(); commands::pin::assign_pin( "button_signal", "2", Some("input"), None, Some(&dir_str), ).unwrap(); // Verify everything is in place let config_after = ProjectConfig::load(tmp.path()).unwrap(); assert!(config_after.libraries.contains_key("button")); let board_pins = config_after.pins.get("uno").unwrap(); assert!(board_pins.assignments.contains_key("button_signal")); // Files exist assert!(tmp.path().join("lib/drivers/button/button.h").exists()); assert!(tmp.path().join("lib/drivers/button/button_digital.h").exists()); assert!(tmp.path().join("lib/drivers/button/button_mock.h").exists()); assert!(tmp.path().join("lib/drivers/button/button_sim.h").exists()); assert!(tmp.path().join("test/test_button.cpp").exists()); assert!(tmp.path().join("test/test_button_app.cpp").exists()); assert!(tmp.path().join("lib/app/btn_flow_app.h").exists()); } #[test] fn test_button_config_records_template_name() { let tmp = extract_button("tmpl_name"); let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.project.template.as_str(), "button"); } #[test] fn test_button_audit_clean_after_full_setup() { let tmp = extract_button("audit_btn"); let meta = library::find_library("button").unwrap(); library::extract_library("button", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.libraries.insert("button".to_string(), meta.version.clone()); config.save(tmp.path()).unwrap(); let dir_str = tmp.path().to_string_lossy().to_string(); commands::pin::assign_pin( "button_signal", "2", Some("input"), None, Some(&dir_str), ).unwrap(); // Audit should pass cleanly commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap(); } // ========================================================================== // Helper: walk all files in a directory tree // ========================================================================== fn walkdir(root: &std::path::Path) -> Vec { let mut files = Vec::new(); fn walk(dir: &std::path::Path, out: &mut Vec) { if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { walk(&path, out); } else if path.is_file() { out.push(path); } } } } walk(root, &mut files); files }