use anvil::commands; use anvil::library; use anvil::project::config::{ ProjectConfig, }; use anvil::templates::{TemplateManager, TemplateContext}; use std::fs; use tempfile::TempDir; // ========================================================================== // Device Library: anvil add / remove / lib // ========================================================================== #[test] fn test_library_registry_lists_tmp36() { let libs = library::list_available(); assert!(!libs.is_empty(), "Should have at least one library"); let tmp36 = libs.iter().find(|l| l.name == "tmp36"); assert!(tmp36.is_some(), "TMP36 should be in the registry"); let meta = tmp36.unwrap(); assert_eq!(meta.bus, "analog"); assert_eq!(meta.pins, vec!["data"]); assert_eq!(meta.interface, "tmp36.h"); assert_eq!(meta.mock, "tmp36_mock.h"); } #[test] fn test_library_find_by_name() { assert!(library::find_library("tmp36").is_some()); assert!(library::find_library("nonexistent_sensor").is_none()); } #[test] fn test_library_extract_creates_driver_directory() { let tmp = TempDir::new().unwrap(); let written = library::extract_library("tmp36", tmp.path()).unwrap(); assert!(!written.is_empty(), "Should write files"); let driver_dir = tmp.path().join("lib/drivers/tmp36"); assert!(driver_dir.exists(), "Driver directory should be created"); // All four files should exist assert!(driver_dir.join("tmp36.h").exists(), "Interface header"); assert!(driver_dir.join("tmp36_analog.h").exists(), "Implementation"); assert!(driver_dir.join("tmp36_mock.h").exists(), "Mock"); assert!(driver_dir.join("tmp36_sim.h").exists(), "Simulation"); } #[test] fn test_library_extract_files_content_is_valid() { let tmp = TempDir::new().unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); let driver_dir = tmp.path().join("lib/drivers/tmp36"); // Interface should define TempSensor class let interface = fs::read_to_string(driver_dir.join("tmp36.h")).unwrap(); assert!(interface.contains("class TempSensor"), "Should define TempSensor"); assert!(interface.contains("readCelsius"), "Should declare readCelsius"); assert!(interface.contains("readFahrenheit"), "Should declare readFahrenheit"); assert!(interface.contains("readRaw"), "Should declare readRaw"); // Implementation should include hal.h let impl_h = fs::read_to_string(driver_dir.join("tmp36_analog.h")).unwrap(); assert!(impl_h.contains("hal.h"), "Implementation should use HAL"); assert!(impl_h.contains("class Tmp36Analog"), "Should define Tmp36Analog"); assert!(impl_h.contains("analogRead"), "Should use analogRead"); // Mock should have setTemperature let mock_h = fs::read_to_string(driver_dir.join("tmp36_mock.h")).unwrap(); assert!(mock_h.contains("class Tmp36Mock"), "Should define Tmp36Mock"); assert!(mock_h.contains("setTemperature"), "Mock should have setTemperature"); // Sim should have noise let sim_h = fs::read_to_string(driver_dir.join("tmp36_sim.h")).unwrap(); assert!(sim_h.contains("class Tmp36Sim"), "Should define Tmp36Sim"); assert!(sim_h.contains("setNoise"), "Sim should have setNoise"); } #[test] fn test_library_remove_cleans_up() { let tmp = TempDir::new().unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); assert!(library::is_installed_on_disk("tmp36", tmp.path())); library::remove_library_files("tmp36", tmp.path()).unwrap(); assert!(!library::is_installed_on_disk("tmp36", tmp.path())); // drivers/ dir should also be cleaned up if empty assert!(!tmp.path().join("lib/drivers").exists()); } #[test] fn test_library_remove_preserves_other_drivers() { let tmp = TempDir::new().unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); // Fake a second driver let other_dir = tmp.path().join("lib/drivers/bmp280"); fs::create_dir_all(&other_dir).unwrap(); fs::write(other_dir.join("bmp280.h"), "// placeholder").unwrap(); library::remove_library_files("tmp36", tmp.path()).unwrap(); // tmp36 gone, bmp280 still there assert!(!library::is_installed_on_disk("tmp36", tmp.path())); assert!(other_dir.exists(), "Other driver should survive"); } #[test] fn test_library_files_are_ascii_only() { let tmp = TempDir::new().unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); let driver_dir = tmp.path().join("lib/drivers/tmp36"); for entry in fs::read_dir(&driver_dir).unwrap() { let entry = entry.unwrap(); let content = fs::read_to_string(entry.path()).unwrap(); 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.file_name().to_string_lossy(), line_num + 1, col + 1, ch as u32 ); } } } } #[test] fn test_config_libraries_field_roundtrips() { let tmp = TempDir::new().unwrap(); let mut config = ProjectConfig::new("lib_test"); config.libraries.insert("tmp36".to_string(), "0.1.0".to_string()); config.save(tmp.path()).unwrap(); let loaded = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(loaded.libraries.len(), 1); assert_eq!(loaded.libraries["tmp36"], "0.1.0"); } #[test] fn test_config_empty_libraries_not_serialized() { let tmp = TempDir::new().unwrap(); let config = ProjectConfig::new("no_libs"); config.save(tmp.path()).unwrap(); let content = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap(); assert!( !content.contains("[libraries]"), "Empty libraries should not appear in TOML" ); } #[test] fn test_config_libraries_serialized_when_present() { let tmp = TempDir::new().unwrap(); let mut config = ProjectConfig::new("has_libs"); config.libraries.insert("tmp36".to_string(), "0.1.0".to_string()); config.save(tmp.path()).unwrap(); let content = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap(); assert!( content.contains("[libraries]"), "Non-empty libraries should appear in TOML" ); assert!(content.contains("tmp36")); } #[test] fn test_cmake_autodiscovers_driver_directories() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "cmake_drv".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 cmake = fs::read_to_string(tmp.path().join("test/CMakeLists.txt")).unwrap(); assert!( cmake.contains("drivers"), "CMakeLists should reference drivers directory" ); assert!( cmake.contains("GLOB DRIVER_DIRS"), "CMakeLists should glob driver directories" ); assert!( cmake.contains("include_directories(${DRIVER_DIR})"), "CMakeLists should add each driver to include path" ); // Driver test auto-discovery assert!( cmake.contains("GLOB DRIVER_TEST_SOURCES"), "CMakeLists should glob driver test files" ); assert!( cmake.contains("gtest_discover_tests(${TEST_NAME})"), "CMakeLists should register driver tests with CTest" ); } // ========================================================================== // Device Library: end-to-end command-level tests // ========================================================================== #[test] fn test_add_library_full_flow() { // Simulates: anvil new mocktest && anvil add tmp36 let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "e2e_add".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(); ProjectConfig::new("e2e_add").save(tmp.path()).unwrap(); // Pre-check: no libraries, no drivers dir let config_before = ProjectConfig::load(tmp.path()).unwrap(); assert!(config_before.libraries.is_empty()); assert!(!tmp.path().join("lib/drivers/tmp36").exists()); // Extract library and update config (mirrors what add_library does) let meta = library::find_library("tmp36").unwrap(); let written = library::extract_library("tmp36", tmp.path()).unwrap(); assert_eq!(written.len(), 5, "Should write 5 files (4 headers + 1 test)"); let mut config = ProjectConfig::load(tmp.path()).unwrap(); let driver_include = format!("lib/drivers/{}", meta.name); if !config.build.include_dirs.contains(&driver_include) { config.build.include_dirs.push(driver_include.clone()); } config.libraries.insert(meta.name.clone(), meta.version.clone()); config.save(tmp.path()).unwrap(); // Post-check: files exist, config updated assert!(tmp.path().join("lib/drivers/tmp36/tmp36.h").exists()); assert!(tmp.path().join("lib/drivers/tmp36/tmp36_analog.h").exists()); assert!(tmp.path().join("lib/drivers/tmp36/tmp36_mock.h").exists()); assert!(tmp.path().join("lib/drivers/tmp36/tmp36_sim.h").exists()); assert!(tmp.path().join("test/test_tmp36.cpp").exists(), "Test file in test/"); let config_after = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config_after.libraries["tmp36"], "0.1.0"); assert!( config_after.build.include_dirs.contains(&driver_include), "include_dirs should contain driver path" ); } #[test] fn test_remove_library_full_flow() { // Simulates: anvil add tmp36 && anvil remove tmp36 let tmp = TempDir::new().unwrap(); ProjectConfig::new("e2e_rm").save(tmp.path()).unwrap(); // Add let meta = library::find_library("tmp36").unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); let driver_include = format!("lib/drivers/{}", meta.name); config.build.include_dirs.push(driver_include.clone()); config.libraries.insert(meta.name.clone(), meta.version.clone()); config.save(tmp.path()).unwrap(); // Remove library::remove_library_files("tmp36", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.build.include_dirs.retain(|d| d != &driver_include); config.libraries.remove("tmp36"); config.save(tmp.path()).unwrap(); // Post-check: files gone, config clean assert!(!tmp.path().join("lib/drivers/tmp36").exists()); assert!(!tmp.path().join("test/test_tmp36.cpp").exists(), "Test file should be removed"); let config_final = ProjectConfig::load(tmp.path()).unwrap(); assert!(config_final.libraries.is_empty()); assert!( !config_final.build.include_dirs.contains(&driver_include), "include_dirs should not contain driver path after remove" ); } #[test] fn test_add_remove_readd_idempotent() { // Simulates: anvil add tmp36 && anvil remove tmp36 && anvil add tmp36 let tmp = TempDir::new().unwrap(); ProjectConfig::new("e2e_idem").save(tmp.path()).unwrap(); let meta = library::find_library("tmp36").unwrap(); let driver_include = format!("lib/drivers/{}", meta.name); // Add library::extract_library("tmp36", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.build.include_dirs.push(driver_include.clone()); config.libraries.insert(meta.name.clone(), meta.version.clone()); config.save(tmp.path()).unwrap(); // Remove library::remove_library_files("tmp36", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.build.include_dirs.retain(|d| d != &driver_include); config.libraries.remove("tmp36"); config.save(tmp.path()).unwrap(); assert!(!tmp.path().join("lib/drivers/tmp36").exists()); // Re-add library::extract_library("tmp36", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.build.include_dirs.push(driver_include.clone()); config.libraries.insert(meta.name.clone(), meta.version.clone()); config.save(tmp.path()).unwrap(); // Everything back to normal assert!(tmp.path().join("lib/drivers/tmp36/tmp36.h").exists()); let config_final = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config_final.libraries["tmp36"], "0.1.0"); assert!(config_final.build.include_dirs.contains(&driver_include)); // No duplicate include_dirs let count = config_final.build.include_dirs.iter() .filter(|d| *d == &driver_include) .count(); assert_eq!(count, 1, "Should not duplicate include dir on re-add"); } #[test] fn test_library_interface_compiles_against_hal() { // Verify the actual C++ content is structurally correct: // tmp36_analog.h includes hal.h, tmp36_mock.h and tmp36_sim.h are standalone let tmp = TempDir::new().unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); let driver_dir = tmp.path().join("lib/drivers/tmp36"); let analog = fs::read_to_string(driver_dir.join("tmp36_analog.h")).unwrap(); assert!(analog.contains("#include \"hal.h\""), "Analog impl must include hal.h"); assert!(analog.contains("#include \"tmp36.h\""), "Analog impl must include interface"); assert!(analog.contains("Hal*"), "Analog impl must accept Hal pointer"); let mock = fs::read_to_string(driver_dir.join("tmp36_mock.h")).unwrap(); assert!(!mock.contains("hal.h"), "Mock should NOT depend on hal.h"); assert!(mock.contains("#include \"tmp36.h\""), "Mock must include interface"); let sim = fs::read_to_string(driver_dir.join("tmp36_sim.h")).unwrap(); assert!(!sim.contains("hal.h"), "Sim should NOT depend on hal.h"); assert!(sim.contains("#include \"tmp36.h\""), "Sim must include interface"); } #[test] fn test_library_polymorphism_contract() { // All implementations must inherit from TempSensor let tmp = TempDir::new().unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); let driver_dir = tmp.path().join("lib/drivers/tmp36"); let interface = fs::read_to_string(driver_dir.join("tmp36.h")).unwrap(); assert!(interface.contains("class TempSensor")); assert!(interface.contains("virtual float readCelsius()")); assert!(interface.contains("virtual int readRaw()")); // Each impl must extend TempSensor for (file, class) in [ ("tmp36_analog.h", "Tmp36Analog"), ("tmp36_mock.h", "Tmp36Mock"), ("tmp36_sim.h", "Tmp36Sim"), ] { let content = fs::read_to_string(driver_dir.join(file)).unwrap(); assert!( content.contains(&format!("class {} : public TempSensor", class)), "{} should extend TempSensor", file ); assert!( content.contains("readCelsius() override"), "{} should override readCelsius", file ); assert!( content.contains("readRaw() override"), "{} should override readRaw", file ); } } // ========================================================================== // Device Library: UX helpers and pin integration // ========================================================================== #[test] fn test_library_meta_wiring_summary() { let meta = library::find_library("tmp36").unwrap(); let summary = meta.wiring_summary(); assert!(summary.contains("analog"), "Should mention analog bus"); assert!(summary.contains("A0"), "Should give A0 as example pin"); } #[test] fn test_library_meta_pin_roles() { let meta = library::find_library("tmp36").unwrap(); let roles = meta.pin_roles(); assert_eq!(roles.len(), 1); assert_eq!(roles[0].0, "data"); assert_eq!(roles[0].1, "tmp36_data"); } #[test] fn test_library_meta_default_mode() { let meta = library::find_library("tmp36").unwrap(); assert_eq!(meta.default_mode(), "analog"); } #[test] fn test_library_unassigned_pins_detects_missing() { let meta = library::find_library("tmp36").unwrap(); let assigned: Vec = vec![]; let missing = library::unassigned_pins(&meta, &assigned); assert_eq!(missing, vec!["tmp36_data"], "Should flag tmp36_data as unassigned"); } #[test] fn test_library_unassigned_pins_detects_assigned() { let meta = library::find_library("tmp36").unwrap(); let assigned = vec!["tmp36_data".to_string()]; let missing = library::unassigned_pins(&meta, &assigned); assert!(missing.is_empty(), "Should detect tmp36_data as assigned"); } #[test] fn test_add_with_pin_creates_assignment() { // Simulates: anvil new test_proj && anvil add tmp36 --pin A0 let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "pin_lib".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(); // Extract library files let meta = library::find_library("tmp36").unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); // Update config like add_library does let mut config = ProjectConfig::load(tmp.path()).unwrap(); let driver_include = format!("lib/drivers/{}", meta.name); if !config.build.include_dirs.contains(&driver_include) { config.build.include_dirs.push(driver_include); } config.libraries.insert(meta.name.clone(), meta.version.clone()); config.save(tmp.path()).unwrap(); // Simulate --pin A0 by calling assign_pin let assign_name = meta.pin_assignment_name(&meta.pins[0]); let dir_str = tmp.path().to_string_lossy().to_string(); commands::pin::assign_pin( &assign_name, "A0", Some(meta.default_mode()), None, Some(&dir_str), ).unwrap(); // Verify the assignment exists let config_after = ProjectConfig::load(tmp.path()).unwrap(); let board_pins = config_after.pins.get("uno").unwrap(); assert!( board_pins.assignments.contains_key("tmp36_data"), "Should have tmp36_data pin assignment" ); let assignment = &board_pins.assignments["tmp36_data"]; assert_eq!(assignment.mode, "analog"); // Verify unassigned_pins now returns empty let assigned: Vec = board_pins.assignments.keys().cloned().collect(); let missing = library::unassigned_pins(&meta, &assigned); assert!(missing.is_empty(), "All library pins should be assigned after --pin"); } #[test] fn test_audit_with_library_missing_pin() { // anvil add tmp36 without --pin should leave pin unassigned let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "audit_lib".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(); // Add library to config but no pin assignment let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.libraries.insert("tmp36".to_string(), "0.1.0".to_string()); config.save(tmp.path()).unwrap(); // Check that unassigned_pins detects it let meta = library::find_library("tmp36").unwrap(); let board_pins = config.pins.get("uno"); let assigned: Vec = board_pins .map(|bp| bp.assignments.keys().cloned().collect()) .unwrap_or_default(); let missing = library::unassigned_pins(&meta, &assigned); assert_eq!(missing, vec!["tmp36_data"]); // The actual audit command must not crash (this was a bug: // audit used to early-return when no pins assigned, skipping library check) let dir_str = tmp.path().to_string_lossy().to_string(); commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap(); } #[test] fn test_audit_with_library_pin_assigned() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "audit_ok".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(); // Add library + pin assignment let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.libraries.insert("tmp36".to_string(), "0.1.0".to_string()); config.save(tmp.path()).unwrap(); let dir_str = tmp.path().to_string_lossy().to_string(); commands::pin::assign_pin("tmp36_data", "A0", Some("analog"), None, Some(&dir_str)).unwrap(); // Now check data model let config_after = ProjectConfig::load(tmp.path()).unwrap(); let board_pins = config_after.pins.get("uno").unwrap(); let assigned: Vec = board_pins.assignments.keys().cloned().collect(); let meta = library::find_library("tmp36").unwrap(); let missing = library::unassigned_pins(&meta, &assigned); assert!(missing.is_empty(), "Pin should be satisfied after assignment"); // And the audit command itself should work with the pin assigned commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap(); } #[test] fn test_audit_no_pins_no_libraries_does_not_crash() { // Baseline: no pins, no libraries -- audit should still work let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "audit_empty".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 dir_str = tmp.path().to_string_lossy().to_string(); commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap(); } #[test] fn test_add_remove_pin_assignment_survives() { // When we remove a library, the pin assignment should still exist // (the user might want to reassign it to a different library) let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "pin_survive".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(); // Add library and assign pin let meta = library::find_library("tmp36").unwrap(); library::extract_library("tmp36", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.libraries.insert("tmp36".to_string(), "0.1.0".to_string()); let driver_include = format!("lib/drivers/{}", meta.name); config.build.include_dirs.push(driver_include.clone()); config.save(tmp.path()).unwrap(); let dir_str = tmp.path().to_string_lossy().to_string(); commands::pin::assign_pin("tmp36_data", "A0", Some("analog"), None, Some(&dir_str)).unwrap(); // Remove library library::remove_library_files("tmp36", tmp.path()).unwrap(); let mut config = ProjectConfig::load(tmp.path()).unwrap(); config.build.include_dirs.retain(|d| d != &driver_include); config.libraries.remove("tmp36"); config.save(tmp.path()).unwrap(); // Pin assignment should still be there let config_final = ProjectConfig::load(tmp.path()).unwrap(); let board_pins = config_final.pins.get("uno").unwrap(); assert!( board_pins.assignments.contains_key("tmp36_data"), "Pin assignment should survive library removal" ); }