From 1ae136530f8ed67277f43882b8f01702e3fa34c3 Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Thu, 19 Feb 2026 17:05:35 -0600 Subject: [PATCH] Even more tests --- tests/integration_test.rs | 984 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 984 insertions(+) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 2286e37..a6323bd 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -2241,4 +2241,988 @@ fn test_pins_per_board_independence() { assert_eq!(mega_pins.assignments.len(), 2); assert!(mega_pins.assignments.contains_key("extra_led")); assert!(!uno_pins.assignments.contains_key("extra_led")); +} + +// ========================================================================== +// Pin: save_pins preserves existing config +// ========================================================================== + +#[test] +fn test_save_pins_preserves_existing_config() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("preserve_test"); + config.save(tmp.path()).unwrap(); + + // Reload, add pins, save + let mut config = ProjectConfig::load(tmp.path()).unwrap(); + let mut assignments = HashMap::new(); + assignments.insert("led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + // Reload and verify original fields survived + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + assert_eq!(loaded.project.name, "preserve_test"); + assert_eq!(loaded.build.default, "uno"); + assert_eq!(loaded.monitor.baud, 115200); + assert!(loaded.build.extra_flags.contains(&"-Werror".to_string())); + // And pins are there + assert_eq!(loaded.pins["uno"].assignments["led"].pin, 13); +} + +// ========================================================================== +// Pin: generated pins.h content +// ========================================================================== + +#[test] +fn test_generate_pins_header_content() { + use anvil::project::config::{BoardPinConfig, PinAssignment, BusConfig}; + use anvil::commands::pin::generate_pins_header; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("gen_test"); + + let mut assignments = HashMap::new(); + assignments.insert("red_led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + assignments.insert("motor".to_string(), PinAssignment { + pin: 9, mode: "pwm".to_string(), + }); + + let mut spi_pins = HashMap::new(); + spi_pins.insert("cs".to_string(), 10u8); + let mut buses = HashMap::new(); + buses.insert("spi".to_string(), BusConfig { user_pins: spi_pins }); + + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses, + }); + config.save(tmp.path()).unwrap(); + + generate_pins_header(None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let header = fs::read_to_string(tmp.path().join("lib/hal/pins.h")).unwrap(); + assert!(header.contains("#pragma once"), "pins.h should have pragma once"); + assert!(header.contains("namespace Pins"), "pins.h should have Pins namespace"); + assert!(header.contains("constexpr uint8_t RED_LED = 13;"), "pins.h should have RED_LED"); + assert!(header.contains("constexpr uint8_t MOTOR = 9;"), "pins.h should have MOTOR"); + assert!(header.contains("SPI_SCK"), "pins.h should have SPI bus pins"); + assert!(header.contains("SPI_MOSI"), "pins.h should have SPI MOSI"); + assert!(header.contains("SPI_CS = 10"), "pins.h should have user-selected CS"); + assert!(header.contains("Auto-generated by Anvil"), "pins.h should have generation comment"); +} + +#[test] +fn test_generate_pins_header_is_ascii_only() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::generate_pins_header; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("ascii_gen"); + + let mut assignments = HashMap::new(); + assignments.insert("sensor".to_string(), PinAssignment { + pin: 5, mode: "input".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + generate_pins_header(None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let header = fs::read_to_string(tmp.path().join("lib/hal/pins.h")).unwrap(); + for (line_num, line) in header.lines().enumerate() { + for (col, ch) in line.chars().enumerate() { + assert!( + ch.is_ascii(), + "Non-ASCII in pins.h at {}:{}: '{}' (U+{:04X})", + line_num + 1, col + 1, ch, ch as u32 + ); + } + } +} + +#[test] +fn test_generate_pins_header_creates_hal_dir() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::generate_pins_header; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("dir_gen"); + + let mut assignments = HashMap::new(); + assignments.insert("led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + // Don't pre-create lib/hal -- generate should create it + assert!(!tmp.path().join("lib/hal").exists()); + generate_pins_header(None, Some(tmp.path().to_str().unwrap())).unwrap(); + assert!(tmp.path().join("lib/hal/pins.h").exists()); +} + +// ========================================================================== +// Pin: init-from copies assignments between boards +// ========================================================================== + +#[test] +fn test_init_from_copies_assignments() { + use anvil::project::config::{BoardPinConfig, PinAssignment, BusConfig}; + use anvil::commands::pin::init_from; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("init_test"); + config.boards.insert("mega".to_string(), BoardProfile { + fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), + baud: None, + }); + + let mut assignments = HashMap::new(); + assignments.insert("red_led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + assignments.insert("button".to_string(), PinAssignment { + pin: 7, mode: "input".to_string(), + }); + + let mut spi_pins = HashMap::new(); + spi_pins.insert("cs".to_string(), 10u8); + let mut buses = HashMap::new(); + buses.insert("spi".to_string(), BusConfig { user_pins: spi_pins }); + + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses, + }); + config.save(tmp.path()).unwrap(); + + init_from("uno", "mega", Some(tmp.path().to_str().unwrap())).unwrap(); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let mega_pins = loaded.pins.get("mega").unwrap(); + assert_eq!(mega_pins.assignments.len(), 2); + assert_eq!(mega_pins.assignments["red_led"].pin, 13); + assert_eq!(mega_pins.assignments["button"].pin, 7); + assert_eq!(mega_pins.buses.len(), 1); + assert!(mega_pins.buses.contains_key("spi")); + + // Source should still be intact + let uno_pins = loaded.pins.get("uno").unwrap(); + assert_eq!(uno_pins.assignments.len(), 2); +} + +#[test] +fn test_init_from_fails_without_target_board() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::init_from; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("init_fail"); + + let mut assignments = HashMap::new(); + assignments.insert("led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + // mega not in boards map -> should fail + let result = init_from("uno", "mega", Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "init_from should fail when target board doesn't exist"); +} + +// ========================================================================== +// Pin: assignment validation +// ========================================================================== + +#[test] +fn test_assign_pin_validates_pin_exists() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("validate_test"); + config.save(tmp.path()).unwrap(); + + // Pin 99 doesn't exist on uno + let result = assign_pin("led", "99", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject pin 99 on uno"); +} + +#[test] +fn test_assign_pin_validates_mode() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("mode_test"); + config.save(tmp.path()).unwrap(); + + // Pin 4 doesn't support PWM on uno + let result = assign_pin("motor", "4", Some("pwm"), None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject PWM on non-PWM pin"); + + // Pin 9 does support PWM + let result = assign_pin("motor", "9", Some("pwm"), None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept PWM on PWM-capable pin"); +} + +#[test] +fn test_assign_pin_validates_name() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("name_test"); + config.save(tmp.path()).unwrap(); + + // Reserved names + let result = assign_pin("spi", "13", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject reserved name 'spi'"); + + // Invalid characters + let result = assign_pin("my-led", "13", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject name with hyphens"); + + // Starting with number + let result = assign_pin("1led", "13", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject name starting with number"); + + // Valid name + let result = assign_pin("status_led", "13", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept valid name"); +} + +#[test] +fn test_assign_pin_accepts_aliases() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("alias_test"); + config.save(tmp.path()).unwrap(); + + // A0 should resolve to pin 14 on uno + let result = assign_pin("temp_sensor", "A0", Some("analog"), None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept A0 alias"); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let pc = loaded.pins.get("uno").unwrap(); + assert_eq!(pc.assignments["temp_sensor"].pin, 14); +} + +// ========================================================================== +// Pin: bus group assignment +// ========================================================================== + +#[test] +fn test_assign_bus_spi_with_cs() { + use anvil::commands::pin::assign_bus; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("bus_test"); + config.save(tmp.path()).unwrap(); + + let user_pins = vec![("cs", "10")]; + let result = assign_bus("spi", &user_pins, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept SPI with CS pin"); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let pc = loaded.pins.get("uno").unwrap(); + assert!(pc.buses.contains_key("spi")); + assert_eq!(*pc.buses["spi"].user_pins.get("cs").unwrap(), 10u8); +} + +#[test] +fn test_assign_bus_spi_without_cs_fails() { + use anvil::commands::pin::assign_bus; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("bus_fail"); + config.save(tmp.path()).unwrap(); + + // SPI requires CS pin + let result = assign_bus("spi", &[], None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "SPI should require CS pin"); +} + +#[test] +fn test_assign_bus_i2c_no_user_pins() { + use anvil::commands::pin::assign_bus; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("i2c_test"); + config.save(tmp.path()).unwrap(); + + // I2C has no user-selectable pins + let result = assign_bus("i2c", &[], None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "I2C should work with no user pins"); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let pc = loaded.pins.get("uno").unwrap(); + assert!(pc.buses.contains_key("i2c")); +} + +#[test] +fn test_assign_bus_unknown_fails() { + use anvil::commands::pin::assign_bus; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("unknown_bus"); + config.save(tmp.path()).unwrap(); + + let result = assign_bus("can", &[], None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject unknown bus name"); +} + +// ========================================================================== +// Pin: remove assignment +// ========================================================================== + +#[test] +fn test_remove_pin_assignment() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::remove_assignment; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("remove_test"); + + let mut assignments = HashMap::new(); + assignments.insert("red_led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + assignments.insert("green_led".to_string(), PinAssignment { + pin: 11, mode: "output".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + remove_assignment("red_led", None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let pc = loaded.pins.get("uno").unwrap(); + assert!(!pc.assignments.contains_key("red_led"), "red_led should be removed"); + assert!(pc.assignments.contains_key("green_led"), "green_led should remain"); +} + +#[test] +fn test_remove_nonexistent_fails() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::remove_assignment; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("remove_fail"); + + let mut assignments = HashMap::new(); + assignments.insert("led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + let result = remove_assignment("nope", None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should fail when removing nonexistent assignment"); +} + +// ========================================================================== +// Pin: board-specific assignments via --board flag +// ========================================================================== + +#[test] +fn test_assign_pin_to_specific_board() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("board_pin"); + config.boards.insert("mega".to_string(), BoardProfile { + fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), + baud: None, + }); + config.save(tmp.path()).unwrap(); + + // Assign to mega specifically + assign_pin("led", "13", None, Some("mega"), Some(tmp.path().to_str().unwrap())).unwrap(); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + assert!(loaded.pins.contains_key("mega"), "Should have mega pin config"); + assert!(!loaded.pins.contains_key("uno"), "Should NOT have uno pin config"); + assert_eq!(loaded.pins["mega"].assignments["led"].pin, 13); +} + +// ========================================================================== +// Pin: overwrite existing assignment +// ========================================================================== + +#[test] +fn test_assign_pin_overwrites_existing() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("overwrite_test"); + config.save(tmp.path()).unwrap(); + + // Assign red_led to pin 13 + assign_pin("red_led", "13", None, None, Some(tmp.path().to_str().unwrap())).unwrap(); + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + assert_eq!(loaded.pins["uno"].assignments["red_led"].pin, 13); + + // Reassign red_led to pin 6 + assign_pin("red_led", "6", Some("pwm"), None, Some(tmp.path().to_str().unwrap())).unwrap(); + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + assert_eq!(loaded.pins["uno"].assignments["red_led"].pin, 6); + assert_eq!(loaded.pins["uno"].assignments["red_led"].mode, "pwm"); + assert_eq!(loaded.pins["uno"].assignments.len(), 1, "Should still be one assignment"); +} + +#[test] +fn test_assign_multiple_pins_sequentially() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("multi_assign"); + config.save(tmp.path()).unwrap(); + + assign_pin("red_led", "13", None, None, Some(tmp.path().to_str().unwrap())).unwrap(); + assign_pin("green_led", "11", None, None, Some(tmp.path().to_str().unwrap())).unwrap(); + assign_pin("button", "7", Some("input"), None, Some(tmp.path().to_str().unwrap())).unwrap(); + assign_pin("pot", "A0", Some("analog"), None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let pc = loaded.pins.get("uno").unwrap(); + assert_eq!(pc.assignments.len(), 4); + assert_eq!(pc.assignments["red_led"].pin, 13); + assert_eq!(pc.assignments["green_led"].pin, 11); + assert_eq!(pc.assignments["button"].pin, 7); + assert_eq!(pc.assignments["button"].mode, "input"); + assert_eq!(pc.assignments["pot"].pin, 14); // A0 = digital 14 on uno + assert_eq!(pc.assignments["pot"].mode, "analog"); +} + +// ========================================================================== +// Pin: mode defaults and validation edge cases +// ========================================================================== + +#[test] +fn test_assign_pin_defaults_to_output() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("default_mode"); + config.save(tmp.path()).unwrap(); + + // No mode specified -> should default to "output" + assign_pin("led", "13", None, None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + assert_eq!(loaded.pins["uno"].assignments["led"].mode, "output"); +} + +#[test] +fn test_assign_pin_rejects_analog_on_digital_only() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("analog_reject"); + config.save(tmp.path()).unwrap(); + + // Pin 4 on uno is digital-only, no analog capability + let result = assign_pin("sensor", "4", Some("analog"), None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject analog mode on digital-only pin"); +} + +#[test] +fn test_assign_pin_rejects_invalid_mode_string() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("bad_mode"); + config.save(tmp.path()).unwrap(); + + let result = assign_pin("led", "13", Some("servo"), None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject unknown mode 'servo'"); +} + +#[test] +fn test_assign_pin_input_pullup_mode() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("pullup_test"); + config.save(tmp.path()).unwrap(); + + let result = assign_pin("button", "7", Some("input_pullup"), None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept input_pullup mode"); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + assert_eq!(loaded.pins["uno"].assignments["button"].mode, "input_pullup"); +} + +// ========================================================================== +// Pin: alias resolution edge cases +// ========================================================================== + +#[test] +fn test_assign_pin_led_builtin_alias() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("builtin_test"); + config.save(tmp.path()).unwrap(); + + let result = assign_pin("status", "LED_BUILTIN", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept LED_BUILTIN alias"); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + assert_eq!(loaded.pins["uno"].assignments["status"].pin, 13); +} + +#[test] +fn test_assign_pin_sda_alias() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("sda_test"); + config.save(tmp.path()).unwrap(); + + let result = assign_pin("temp_data", "SDA", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept SDA alias"); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + // Uno SDA = pin 18 (A4) + assert_eq!(loaded.pins["uno"].assignments["temp_data"].pin, 18); +} + +// ========================================================================== +// Pin: name validation edge cases +// ========================================================================== + +#[test] +fn test_assign_pin_underscore_prefix_ok() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("underscore_test"); + config.save(tmp.path()).unwrap(); + + let result = assign_pin("_internal", "13", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept name starting with underscore"); +} + +#[test] +fn test_assign_pin_all_uppercase_ok() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("upper_test"); + config.save(tmp.path()).unwrap(); + + let result = assign_pin("STATUS_LED", "13", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Should accept all-uppercase name"); +} + +#[test] +fn test_assign_pin_rejects_spaces() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("space_test"); + config.save(tmp.path()).unwrap(); + + let result = assign_pin("my led", "13", None, None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should reject name with spaces"); +} + +// ========================================================================== +// Pin: remove bus assignment +// ========================================================================== + +#[test] +fn test_remove_bus_assignment() { + use anvil::project::config::{BoardPinConfig, BusConfig}; + use anvil::commands::pin::remove_assignment; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("remove_bus"); + + let mut buses = HashMap::new(); + let mut spi_pins = HashMap::new(); + spi_pins.insert("cs".to_string(), 10u8); + buses.insert("spi".to_string(), BusConfig { user_pins: spi_pins }); + buses.insert("i2c".to_string(), BusConfig { user_pins: HashMap::new() }); + + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments: HashMap::new(), + buses, + }); + config.save(tmp.path()).unwrap(); + + remove_assignment("spi", None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let pc = loaded.pins.get("uno").unwrap(); + assert!(!pc.buses.contains_key("spi"), "spi should be removed"); + assert!(pc.buses.contains_key("i2c"), "i2c should remain"); +} + +// ========================================================================== +// Pin: generate_pins_header edge cases +// ========================================================================== + +#[test] +fn test_generate_fails_with_no_assignments() { + use anvil::commands::pin::generate_pins_header; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("empty_gen"); + config.save(tmp.path()).unwrap(); + + let result = generate_pins_header(None, Some(tmp.path().to_str().unwrap())); + assert!(result.is_err(), "Should fail when no pin assignments exist"); +} + +#[test] +fn test_generate_header_includes_board_info_comment() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::generate_pins_header; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("comment_gen"); + + let mut assignments = HashMap::new(); + assignments.insert("led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + generate_pins_header(None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let header = fs::read_to_string(tmp.path().join("lib/hal/pins.h")).unwrap(); + assert!(header.contains("uno"), "pins.h should mention board name"); + assert!(header.contains("arduino:avr:uno"), "pins.h should mention FQBN"); +} + +#[test] +fn test_generate_header_names_are_uppercased() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::generate_pins_header; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("upper_gen"); + + let mut assignments = HashMap::new(); + assignments.insert("motor_pwm".to_string(), PinAssignment { + pin: 9, mode: "pwm".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + generate_pins_header(None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let header = fs::read_to_string(tmp.path().join("lib/hal/pins.h")).unwrap(); + assert!(header.contains("MOTOR_PWM"), "pins.h should uppercase pin names"); + assert!(!header.contains("motor_pwm"), "pins.h should not have lowercase pin names"); +} + +#[test] +fn test_generate_header_includes_stdint() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::generate_pins_header; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("stdint_gen"); + + let mut assignments = HashMap::new(); + assignments.insert("led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + generate_pins_header(None, Some(tmp.path().to_str().unwrap())).unwrap(); + + let header = fs::read_to_string(tmp.path().join("lib/hal/pins.h")).unwrap(); + assert!(header.contains("#include "), "pins.h should include stdint.h for uint8_t"); +} + +// ========================================================================== +// Pin: audit smoke tests (don't crash) +// ========================================================================== + +#[test] +fn test_audit_with_no_assignments_does_not_crash() { + use anvil::commands::pin::audit_pins; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("audit_empty"); + config.save(tmp.path()).unwrap(); + + // Should succeed with helpful "no assignments" message, not crash + let result = audit_pins(None, false, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Audit with no assignments should not crash"); +} + +#[test] +fn test_audit_with_assignments_does_not_crash() { + use anvil::project::config::{BoardPinConfig, PinAssignment, BusConfig}; + use anvil::commands::pin::audit_pins; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("audit_full"); + + let mut assignments = HashMap::new(); + assignments.insert("red_led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + assignments.insert("button".to_string(), PinAssignment { + pin: 7, mode: "input".to_string(), + }); + + let mut spi_pins = HashMap::new(); + spi_pins.insert("cs".to_string(), 10u8); + let mut buses = HashMap::new(); + buses.insert("spi".to_string(), BusConfig { user_pins: spi_pins }); + + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments, buses, + }); + config.save(tmp.path()).unwrap(); + + let result = audit_pins(None, false, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Audit with assignments should not crash"); + + // Brief mode should also work + let result = audit_pins(None, true, Some(tmp.path().to_str().unwrap())); + assert!(result.is_ok(), "Audit --brief should not crash"); +} + +// ========================================================================== +// Pin: show_pin_map and show_capabilities smoke tests +// ========================================================================== + +#[test] +fn test_show_pin_map_without_project() { + use anvil::commands::pin::show_pin_map; + + // Should work with just a board name, no project needed + let result = show_pin_map(Some("uno"), None, Some("/nonexistent")); + assert!(result.is_ok(), "show_pin_map should work without a project"); +} + +#[test] +fn test_show_pin_map_with_filter() { + use anvil::commands::pin::show_pin_map; + + let result = show_pin_map(Some("uno"), Some("pwm"), Some("/nonexistent")); + assert!(result.is_ok(), "show_pin_map with pwm filter should work"); +} + +#[test] +fn test_show_pin_map_invalid_filter() { + use anvil::commands::pin::show_pin_map; + + let result = show_pin_map(Some("uno"), Some("dac"), Some("/nonexistent")); + assert!(result.is_err(), "show_pin_map with invalid capability should fail"); +} + +#[test] +fn test_show_capabilities_without_project() { + use anvil::commands::pin::show_capabilities; + + let result = show_capabilities(Some("mega"), Some("/nonexistent")); + assert!(result.is_ok(), "show_capabilities should work without a project"); +} + +#[test] +fn test_show_pin_map_unknown_board() { + use anvil::commands::pin::show_pin_map; + + let result = show_pin_map(Some("esp32"), None, Some("/nonexistent")); + assert!(result.is_err(), "show_pin_map should fail for unknown board"); +} + +// ========================================================================== +// Pin: save_pins TOML writer preserves non-pin sections +// ========================================================================== + +#[test] +fn test_save_pins_writer_preserves_boards_section() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("writer_test"); + config.boards.insert("mega".to_string(), BoardProfile { + fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), + baud: Some(57600), + }); + config.save(tmp.path()).unwrap(); + + // Assign a pin (this exercises save_pins internally) + assign_pin("led", "13", None, None, Some(tmp.path().to_str().unwrap())).unwrap(); + + // Verify boards section survived + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + assert!(loaded.boards.contains_key("mega"), "mega board should survive pin save"); + assert_eq!(loaded.boards["mega"].baud, Some(57600)); + assert_eq!(loaded.build.default, "uno"); +} + +#[test] +fn test_save_pins_writer_idempotent() { + use anvil::commands::pin::assign_pin; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("idempotent"); + config.save(tmp.path()).unwrap(); + + // Assign same pin twice -> file should stabilize + assign_pin("led", "13", None, None, Some(tmp.path().to_str().unwrap())).unwrap(); + let content1 = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap(); + + assign_pin("led", "13", None, None, Some(tmp.path().to_str().unwrap())).unwrap(); + let content2 = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap(); + + assert_eq!(content1, content2, "Saving same pin twice should produce identical output"); +} + +// ========================================================================== +// Pin: init_from does NOT clobber existing target assignments +// ========================================================================== + +#[test] +fn test_init_from_overwrites_existing_target_pins() { + use anvil::project::config::{BoardPinConfig, PinAssignment}; + use anvil::commands::pin::init_from; + use std::collections::HashMap; + + let tmp = TempDir::new().unwrap(); + let mut config = ProjectConfig::new("overwrite_init"); + config.boards.insert("mega".to_string(), BoardProfile { + fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), + baud: None, + }); + + // Source: uno has red_led on 13 + let mut uno_assigns = HashMap::new(); + uno_assigns.insert("red_led".to_string(), PinAssignment { + pin: 13, mode: "output".to_string(), + }); + config.pins.insert("uno".to_string(), BoardPinConfig { + assignments: uno_assigns, buses: HashMap::new(), + }); + + // Target: mega already has something + let mut mega_assigns = HashMap::new(); + mega_assigns.insert("old_pin".to_string(), PinAssignment { + pin: 22, mode: "output".to_string(), + }); + config.pins.insert("mega".to_string(), BoardPinConfig { + assignments: mega_assigns, buses: HashMap::new(), + }); + config.save(tmp.path()).unwrap(); + + // init_from replaces the entire target pin config + init_from("uno", "mega", Some(tmp.path().to_str().unwrap())).unwrap(); + + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let mega_pins = loaded.pins.get("mega").unwrap(); + assert!(mega_pins.assignments.contains_key("red_led"), "Should have copied red_led"); + assert!(!mega_pins.assignments.contains_key("old_pin"), "old_pin should be replaced"); +} + +// ========================================================================== +// Pin: assign_bus with user_pins round-trips through TOML +// ========================================================================== + +#[test] +fn test_bus_user_pins_survive_toml_roundtrip() { + use anvil::commands::pin::assign_bus; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("bus_roundtrip"); + config.save(tmp.path()).unwrap(); + + let user_pins = vec![("cs", "10")]; + assign_bus("spi", &user_pins, None, Some(tmp.path().to_str().unwrap())).unwrap(); + + // Verify the raw TOML content is parseable + let content = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap(); + assert!( + content.contains("[pins."), + "TOML should have pins section after bus assign" + ); + + // Reload and verify + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let pc = loaded.pins.get("uno").unwrap(); + let spi = pc.buses.get("spi").unwrap(); + assert_eq!(*spi.user_pins.get("cs").unwrap(), 10u8); +} + +// ========================================================================== +// Pin: mixed pins and buses in same board config +// ========================================================================== + +#[test] +fn test_mixed_pins_and_buses_roundtrip() { + use anvil::commands::pin::{assign_pin, assign_bus}; + + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig::new("mixed_test"); + config.save(tmp.path()).unwrap(); + + // Assign individual pins + assign_pin("red_led", "13", None, None, Some(tmp.path().to_str().unwrap())).unwrap(); + assign_pin("button", "7", Some("input"), None, Some(tmp.path().to_str().unwrap())).unwrap(); + + // Assign bus groups + let spi_pins = vec![("cs", "10")]; + assign_bus("spi", &spi_pins, None, Some(tmp.path().to_str().unwrap())).unwrap(); + assign_bus("i2c", &[], None, Some(tmp.path().to_str().unwrap())).unwrap(); + + // Everything should survive the round-trip + let loaded = ProjectConfig::load(tmp.path()).unwrap(); + let pc = loaded.pins.get("uno").unwrap(); + + assert_eq!(pc.assignments.len(), 2); + assert_eq!(pc.assignments["red_led"].pin, 13); + assert_eq!(pc.assignments["button"].pin, 7); + assert_eq!(pc.assignments["button"].mode, "input"); + + assert_eq!(pc.buses.len(), 2); + assert!(pc.buses.contains_key("spi")); + assert!(pc.buses.contains_key("i2c")); + assert_eq!(*pc.buses["spi"].user_pins.get("cs").unwrap(), 10u8); } \ No newline at end of file