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:
@@ -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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user