Files
anvil/tests/test_library.rs
2026-02-22 07:47:51 -06:00

828 lines
31 KiB
Rust

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<String> = 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<String> = 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<String> = 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<String> = 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"
);
}
// ==========================================================================
// Button Library: registry, extraction, content, coexistence
// ==========================================================================
#[test]
fn test_library_registry_lists_button() {
let libs = library::list_available();
let btn = libs.iter().find(|l| l.name == "button");
assert!(btn.is_some(), "Button should be in the registry");
let meta = btn.unwrap();
assert_eq!(meta.bus, "digital");
assert_eq!(meta.pins, vec!["signal"]);
assert_eq!(meta.interface, "button.h");
assert_eq!(meta.mock, "button_mock.h");
}
#[test]
fn test_button_extract_creates_driver_directory() {
let tmp = TempDir::new().unwrap();
let written = library::extract_library("button", tmp.path()).unwrap();
assert!(!written.is_empty(), "Should write files");
let driver_dir = tmp.path().join("lib/drivers/button");
assert!(driver_dir.exists(), "Driver directory should be created");
assert!(driver_dir.join("button.h").exists(), "Interface header");
assert!(driver_dir.join("button_digital.h").exists(), "Implementation");
assert!(driver_dir.join("button_mock.h").exists(), "Mock");
assert!(driver_dir.join("button_sim.h").exists(), "Simulation");
}
#[test]
fn test_button_extract_files_content_is_valid() {
let tmp = TempDir::new().unwrap();
library::extract_library("button", tmp.path()).unwrap();
let driver_dir = tmp.path().join("lib/drivers/button");
// Interface should define Button class
let interface = fs::read_to_string(driver_dir.join("button.h")).unwrap();
assert!(interface.contains("class Button"), "Should define Button");
assert!(interface.contains("isPressed"), "Should declare isPressed");
assert!(interface.contains("readState"), "Should declare readState");
// Implementation should include hal.h
let impl_h = fs::read_to_string(driver_dir.join("button_digital.h")).unwrap();
assert!(impl_h.contains("hal.h"), "Implementation should use HAL");
assert!(impl_h.contains("class ButtonDigital"), "Should define ButtonDigital");
assert!(impl_h.contains("digitalRead"), "Should use digitalRead");
// Mock should have setPressed
let mock_h = fs::read_to_string(driver_dir.join("button_mock.h")).unwrap();
assert!(mock_h.contains("class ButtonMock"), "Should define ButtonMock");
assert!(mock_h.contains("setPressed"), "Mock should have setPressed");
// Sim should have press/release and bounce
let sim_h = fs::read_to_string(driver_dir.join("button_sim.h")).unwrap();
assert!(sim_h.contains("class ButtonSim"), "Should define ButtonSim");
assert!(sim_h.contains("setBounceReads"), "Sim should have setBounceReads");
assert!(sim_h.contains("press()"), "Sim should have press()");
assert!(sim_h.contains("release()"), "Sim should have release()");
}
#[test]
fn test_button_files_are_ascii_only() {
let tmp = TempDir::new().unwrap();
library::extract_library("button", tmp.path()).unwrap();
let driver_dir = tmp.path().join("lib/drivers/button");
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_button_remove_cleans_up() {
let tmp = TempDir::new().unwrap();
library::extract_library("button", tmp.path()).unwrap();
assert!(library::is_installed_on_disk("button", tmp.path()));
assert!(tmp.path().join("test/test_button.cpp").exists());
library::remove_library_files("button", tmp.path()).unwrap();
assert!(!library::is_installed_on_disk("button", tmp.path()));
assert!(!tmp.path().join("test/test_button.cpp").exists());
}
#[test]
fn test_button_meta_wiring_summary() {
let meta = library::find_library("button").unwrap();
let summary = meta.wiring_summary();
assert!(summary.contains("digital"), "Should mention digital bus: {}", summary);
}
#[test]
fn test_button_meta_pin_roles() {
let meta = library::find_library("button").unwrap();
let roles = meta.pin_roles();
assert_eq!(roles.len(), 1);
assert_eq!(roles[0].0, "signal");
assert_eq!(roles[0].1, "button_signal");
}
#[test]
fn test_button_meta_default_mode() {
let meta = library::find_library("button").unwrap();
assert_eq!(meta.default_mode(), "input");
}
#[test]
fn test_button_interface_uses_polymorphism() {
let tmp = TempDir::new().unwrap();
library::extract_library("button", tmp.path()).unwrap();
let driver_dir = tmp.path().join("lib/drivers/button");
// All implementations should inherit from Button
let impl_h = fs::read_to_string(driver_dir.join("button_digital.h")).unwrap();
assert!(impl_h.contains(": public Button"), "ButtonDigital should inherit Button");
let mock_h = fs::read_to_string(driver_dir.join("button_mock.h")).unwrap();
assert!(mock_h.contains(": public Button"), "ButtonMock should inherit Button");
let sim_h = fs::read_to_string(driver_dir.join("button_sim.h")).unwrap();
assert!(sim_h.contains(": public Button"), "ButtonSim should inherit Button");
}
#[test]
fn test_add_button_full_flow() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "btn_flow".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 meta = library::find_library("button").unwrap();
library::extract_library("button", 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);
config.libraries.insert(meta.name.clone(), meta.version.clone());
config.save(tmp.path()).unwrap();
// Assign a digital 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();
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"));
let assignment = &board_pins.assignments["button_signal"];
assert_eq!(assignment.mode, "input");
}
#[test]
fn test_both_libraries_install_together() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "both_libs".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();
library::extract_library("tmp36", tmp.path()).unwrap();
library::extract_library("button", tmp.path()).unwrap();
// Both driver directories exist
assert!(tmp.path().join("lib/drivers/tmp36").is_dir());
assert!(tmp.path().join("lib/drivers/button").is_dir());
// Both test files exist
assert!(tmp.path().join("test/test_tmp36.cpp").exists());
assert!(tmp.path().join("test/test_button.cpp").exists());
// Remove button, tmp36 survives
library::remove_library_files("button", tmp.path()).unwrap();
assert!(!library::is_installed_on_disk("button", tmp.path()));
assert!(library::is_installed_on_disk("tmp36", tmp.path()));
assert!(tmp.path().join("test/test_tmp36.cpp").exists());
assert!(!tmp.path().join("test/test_button.cpp").exists());
}