828 lines
31 KiB
Rust
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());
|
|
} |