Add mock Arduino for x86_64 host-side testing

Complete Arduino API mock (mock_arduino.h/cpp) enabling application
code to compile and run on PC without hardware. Includes MockSerial,
String class, GPIO/analog/timing/interrupt mocks with state tracking
and test control API.

- Arduino.h, Wire.h, SPI.h shims intercept includes in test builds
- System test template (test_system.cpp) using SimHal
- CMakeLists.txt builds mock_arduino as static lib, links both suites
- Root test.sh/test.bat with --unit/--system/--clean/--verbose flags
- test.bat auto-detects MSVC via vswhere + vcvarsall.bat
- Doctor reports nuanced compiler status (on PATH vs installed)
- Refresh pulls mock infrastructure into existing projects
- 15 tests passing: 7 unit (MockHal) + 8 system (SimHal)
This commit is contained in:
Eric Ratliff
2026-02-20 08:21:11 -06:00
parent 1ae136530f
commit aa1e9d5043
13 changed files with 1831 additions and 16 deletions

View File

@@ -147,6 +147,38 @@ fn test_template_creates_test_infrastructure() {
tmp.path().join("test/mocks/sim_hal.h").exists(),
"sim_hal.h should exist"
);
assert!(
tmp.path().join("test/mocks/mock_arduino.h").exists(),
"mock_arduino.h should exist"
);
assert!(
tmp.path().join("test/mocks/mock_arduino.cpp").exists(),
"mock_arduino.cpp should exist"
);
assert!(
tmp.path().join("test/mocks/Arduino.h").exists(),
"Arduino.h shim should exist"
);
assert!(
tmp.path().join("test/mocks/Wire.h").exists(),
"Wire.h shim should exist"
);
assert!(
tmp.path().join("test/mocks/SPI.h").exists(),
"SPI.h shim should exist"
);
assert!(
tmp.path().join("test/test_system.cpp").exists(),
"test_system.cpp should exist"
);
assert!(
tmp.path().join("test.sh").exists(),
"test.sh root script should exist"
);
assert!(
tmp.path().join("test.bat").exists(),
"test.bat root script should exist"
);
assert!(
tmp.path().join("test/run_tests.sh").exists(),
"run_tests.sh should exist"
@@ -834,10 +866,18 @@ fn test_full_project_structure() {
"_monitor_filter.ps1",
"test/CMakeLists.txt",
"test/test_unit.cpp",
"test/test_system.cpp",
"test/run_tests.sh",
"test/run_tests.bat",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test.sh",
"test.bat",
];
for f in &expected_files {
@@ -1414,9 +1454,18 @@ fn test_refresh_freshly_extracted_is_up_to_date() {
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
"test.sh", "test.bat",
"_detect_port.ps1",
"_monitor_filter.ps1",
"test/run_tests.sh", "test/run_tests.bat",
"test/CMakeLists.txt",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
];
for f in &refreshable {
@@ -1461,7 +1510,7 @@ fn test_refresh_detects_modified_script() {
#[test]
fn test_refresh_does_not_list_user_files() {
// .anvil.toml, source files, and config must never be refreshable.
// .anvil.toml, source files, and user test code must never be refreshable.
let never_refreshable = vec![
".anvil.toml",
".anvil.local",
@@ -1469,19 +1518,26 @@ fn test_refresh_does_not_list_user_files() {
".editorconfig",
".clang-format",
"README.md",
"test/CMakeLists.txt",
"test/test_unit.cpp",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
"test/test_system.cpp",
];
let refreshable = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
"test.sh", "test.bat",
"_detect_port.ps1",
"_monitor_filter.ps1",
"test/run_tests.sh", "test/run_tests.bat",
"test/CMakeLists.txt",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
];
for uf in &never_refreshable {
@@ -3225,4 +3281,206 @@ fn test_mixed_pins_and_buses_roundtrip() {
assert!(pc.buses.contains_key("spi"));
assert!(pc.buses.contains_key("i2c"));
assert_eq!(*pc.buses["spi"].user_pins.get("cs").unwrap(), 10u8);
}
// ==========================================================================
// Mock Arduino: template file content verification
// ==========================================================================
#[test]
fn test_mock_arduino_header_has_core_api() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "mock_test".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 header = fs::read_to_string(tmp.path().join("test/mocks/mock_arduino.h")).unwrap();
// Core Arduino constants
assert!(header.contains("#define INPUT"), "Should define INPUT");
assert!(header.contains("#define OUTPUT"), "Should define OUTPUT");
assert!(header.contains("#define HIGH"), "Should define HIGH");
assert!(header.contains("#define LOW"), "Should define LOW");
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");
// Serial class
assert!(header.contains("class MockSerial"), "Should declare MockSerial");
assert!(header.contains("extern MockSerial Serial"), "Should declare global Serial");
// Test control API
assert!(header.contains("mock_arduino_reset()"), "Should have reset");
assert!(header.contains("mock_arduino_advance_millis("), "Should have advance_millis");
assert!(header.contains("mock_arduino_set_digital("), "Should have set_digital");
assert!(header.contains("mock_arduino_set_analog("), "Should have set_analog");
// String class
assert!(header.contains("class String"), "Should declare String class");
}
#[test]
fn test_mock_arduino_shims_exist() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "shim_test".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();
// Arduino.h shim should include mock_arduino.h
let arduino_h = fs::read_to_string(tmp.path().join("test/mocks/Arduino.h")).unwrap();
assert!(
arduino_h.contains("mock_arduino.h"),
"Arduino.h shim should redirect to mock_arduino.h"
);
// Wire.h shim should provide MockWire
let wire_h = fs::read_to_string(tmp.path().join("test/mocks/Wire.h")).unwrap();
assert!(wire_h.contains("class MockWire"), "Wire.h should declare MockWire");
assert!(wire_h.contains("extern MockWire Wire"), "Wire.h should declare global Wire");
// SPI.h shim should provide MockSPI
let spi_h = fs::read_to_string(tmp.path().join("test/mocks/SPI.h")).unwrap();
assert!(spi_h.contains("class MockSPI"), "SPI.h should declare MockSPI");
assert!(spi_h.contains("extern MockSPI SPI"), "SPI.h should declare global SPI");
assert!(spi_h.contains("SPI_MODE0"), "SPI.h should define SPI modes");
assert!(spi_h.contains("struct SPISettings"), "SPI.h should define SPISettings");
}
#[test]
fn test_mock_arduino_all_files_ascii() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "ascii_mock".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 mock_files = vec![
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
];
for filename in &mock_files {
let content = fs::read_to_string(tmp.path().join(filename)).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})",
filename, line_num + 1, col + 1, ch, ch as u32
);
}
}
}
}
#[test]
fn test_cmake_links_mock_arduino() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "cmake_test".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();
// Should define mock_arduino library
assert!(cmake.contains("add_library(mock_arduino"), "Should define mock_arduino library");
assert!(cmake.contains("mock_arduino.cpp"), "Should compile mock_arduino.cpp");
// Both test targets should link mock_arduino
assert!(cmake.contains("target_link_libraries(test_unit"), "Should have test_unit target");
assert!(cmake.contains("target_link_libraries(test_system"), "Should have test_system target");
// System test target
assert!(cmake.contains("add_executable(test_system"), "Should build test_system");
assert!(cmake.contains("test_system.cpp"), "Should compile test_system.cpp");
// gtest discovery for both
assert!(cmake.contains("gtest_discover_tests(test_unit)"), "Should discover unit tests");
assert!(cmake.contains("gtest_discover_tests(test_system)"), "Should discover system tests");
}
#[test]
fn test_system_test_template_uses_simhal() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "sys_test".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 system_test = fs::read_to_string(tmp.path().join("test/test_system.cpp")).unwrap();
// Should include mock_arduino and sim_hal
assert!(system_test.contains("mock_arduino.h"), "Should include mock_arduino.h");
assert!(system_test.contains("sim_hal.h"), "Should include sim_hal.h");
assert!(system_test.contains("sys_test_app.h"), "Should reference project app header");
// Should use SimHal, not MockHal
assert!(system_test.contains("SimHal"), "Should use SimHal for system tests");
// Should call mock_arduino_reset
assert!(system_test.contains("mock_arduino_reset()"), "Should reset mock state in SetUp");
}
#[test]
fn test_root_test_scripts_exist_and_reference_test_dir() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "script_test".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();
// test.sh
let test_sh = fs::read_to_string(tmp.path().join("test.sh")).unwrap();
assert!(test_sh.contains("TEST_DIR="), "test.sh should set TEST_DIR");
assert!(test_sh.contains("cmake"), "test.sh should invoke cmake");
assert!(test_sh.contains("ctest"), "test.sh should invoke ctest");
assert!(test_sh.contains("--unit"), "test.sh should support --unit flag");
assert!(test_sh.contains("--system"), "test.sh should support --system flag");
assert!(test_sh.contains("--clean"), "test.sh should support --clean flag");
// test.bat
let test_bat = fs::read_to_string(tmp.path().join("test.bat")).unwrap();
assert!(test_bat.contains("TEST_DIR"), "test.bat should set TEST_DIR");
assert!(test_bat.contains("cmake"), "test.bat should invoke cmake");
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");
}