feat: Layer 2 device library system with TMP36 reference driver

Add embedded device library registry with full lifecycle management,
automated test integration, and pin assignment workflow.

Library system:
- Library registry embedded in binary via include_dir! (same as templates)
- library.toml metadata format: name, version, bus type, pins, provided files
- anvil add <name> extracts headers to lib/drivers/<name>/, test files to test/
- anvil remove <name> cleans up all files, config entries, and include paths
- anvil lib lists installed libraries with status
- anvil lib --available shows registry with student-friendly wiring summary
- [libraries] section in .anvil.toml tracks installed libraries and versions
- CMake auto-discovers lib/drivers/*/ for include paths at configure time

TMP36 analog temperature sensor (reference implementation):
- TempSensor abstract interface (readCelsius/readFahrenheit/readRaw)
- Tmp36Analog: real hardware impl via Hal::analogRead with configurable Vref
- Tmp36Mock: programmable values, read counting, no hardware needed
- Tmp36Sim: deterministic noise via seeded LCG for repeatable system tests
- test_tmp36.cpp: 21 Google Test cases covering mock, analog, sim, polymorphism
- TMP36 formula: voltage_mV = raw * Vref_mV / 1024, temp_C = (mV - 500) / 10

Automated test integration:
- Library test files (test_*.cpp) route to test/ during extraction
- CMakeLists.txt auto-discovers test_*.cpp via GLOB, builds each as own target
- anvil remove cleans up test files alongside driver headers
- Zero manual CMake editing: add library, run test --clean, tests appear

Pin assignment integration:
- anvil add <name> --pin A0 does extract + pin assignment in one step
- Without --pin, prints step-by-step wiring guidance with copy-paste commands
- anvil pin --audit flags installed libraries with unassigned pins
- Audit works even with zero existing pin assignments (fixed early-return bug)
- LibraryMeta helpers: wiring_summary(), pin_roles(), default_mode()
- Bus-aware guidance: analog pins, I2C bus registration, SPI with CS selection

UX improvements:
- anvil lib --available shows "Needs: 1 analog pin (e.g. A0)" not raw metadata
- anvil add prints app code example, test code example, and next step
- anvil pin --audit prints exact commands to resolve missing library pins
- anvil remove shows test file deletion in output

Files added:
- libraries/tmp36/library.toml
- libraries/tmp36/src/tmp36.h, tmp36_analog.h, tmp36_mock.h, tmp36_sim.h
- libraries/tmp36/src/test_tmp36.cpp
- src/library/mod.rs

Files modified:
- src/lib.rs, src/main.rs, src/commands/mod.rs
- src/commands/lib.rs (add/remove/list/list_available with --pin support)
- src/commands/pin.rs (audit library pin warnings, print_library_pin_warnings)
- src/project/config.rs (libraries HashMap field)
- templates/basic/test/CMakeLists.txt.tmpl (driver + test auto-discovery)

Tests: 254 total (89 unit + 165 integration)
- 12 library/mod.rs unit tests (registry, extraction, helpers)
- 2 commands/lib.rs unit tests (class name derivation)
- 30+ new integration tests covering library lifecycle, pin integration,
  audit flows, file routing, CMake discovery, config roundtrips, ASCII
  compliance, polymorphism contracts, and idempotent add/remove cycles
This commit is contained in:
Eric Ratliff
2026-02-21 11:46:22 -06:00
parent aa1e9d5043
commit 706f420aaa
15 changed files with 2225 additions and 19 deletions

View File

@@ -6,6 +6,8 @@ use anvil::templates::{TemplateManager, TemplateContext};
use anvil::project::config::{
ProjectConfig, BoardProfile, CONFIG_FILENAME, set_default_in_file,
};
use anvil::library;
use anvil::commands;
// ============================================================================
// Template extraction tests
@@ -3309,14 +3311,14 @@ fn test_mock_arduino_header_has_core_api() {
assert!(header.contains("#define LED_BUILTIN"), "Should define LED_BUILTIN");
assert!(header.contains("#define A0"), "Should define A0");
// Core Arduino functions
assert!(header.contains("void pinMode("), "Should declare pinMode");
assert!(header.contains("void digitalWrite("), "Should declare digitalWrite");
assert!(header.contains("int digitalRead("), "Should declare digitalRead");
assert!(header.contains("int analogRead("), "Should declare analogRead");
assert!(header.contains("void analogWrite("), "Should declare analogWrite");
assert!(header.contains("unsigned long millis()"), "Should declare millis");
assert!(header.contains("void delay("), "Should declare delay");
// Core Arduino functions (declarations use aligned whitespace)
assert!(header.contains("pinMode("), "Should declare pinMode");
assert!(header.contains("digitalWrite("), "Should declare digitalWrite");
assert!(header.contains("digitalRead("), "Should declare digitalRead");
assert!(header.contains("analogRead("), "Should declare analogRead");
assert!(header.contains("analogWrite("), "Should declare analogWrite");
assert!(header.contains("millis()"), "Should declare millis");
assert!(header.contains("delay("), "Should declare delay");
// Serial class
assert!(header.contains("class MockSerial"), "Should declare MockSerial");
@@ -3483,4 +3485,613 @@ fn test_root_test_scripts_exist_and_reference_test_dir() {
assert!(test_bat.contains("ctest"), "test.bat should invoke ctest");
assert!(test_bat.contains("--unit"), "test.bat should support --unit flag");
assert!(test_bat.contains("--system"), "test.bat should support --system flag");
}
// ==========================================================================
// 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"
);
}