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

@@ -0,0 +1,353 @@
#ifndef MOCK_ARDUINO_H
#define MOCK_ARDUINO_H
/*
* mock_arduino.h -- Arduino API mock for host-side (x86_64) testing.
*
* Provides the core Arduino vocabulary (types, constants, functions,
* Serial) so that application code compiles and runs on a PC without
* any AVR toolchain. All state is captured in a global MockArduino
* instance that tests can inspect and manipulate.
*
* This file replaces <Arduino.h> in the test build. The CMakeLists.txt
* adds the mocks/ directory to the include path, so any code that does
* #include <Arduino.h> will pick up this file instead.
*
* Usage in tests:
*
* #include "mock_arduino.h"
*
* TEST(MyTest, ReadsAnalogSensor) {
* mock_arduino_reset();
* mock_arduino_set_analog(A0, 512);
* int val = analogRead(A0);
* EXPECT_EQ(val, 512);
* }
*
* Generated by Anvil -- https://github.com/nexus-workshops/anvil
*/
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include <functional>
// ============================================================================
// Arduino types
// ============================================================================
typedef uint8_t byte;
typedef uint16_t word;
typedef bool boolean;
// ============================================================================
// Pin mode and logic constants
// ============================================================================
#ifndef INPUT
#define INPUT 0x0
#define OUTPUT 0x1
#define INPUT_PULLUP 0x2
#endif
#ifndef LOW
#define LOW 0x0
#define HIGH 0x1
#endif
#ifndef LED_BUILTIN
#define LED_BUILTIN 13
#endif
// Analog pins (map to digital pin numbers, Uno layout by default)
#ifndef A0
#define A0 14
#define A1 15
#define A2 16
#define A3 17
#define A4 18
#define A5 19
#define A6 20
#define A7 21
// Extended for Mega
#define A8 54
#define A9 55
#define A10 56
#define A11 57
#define A12 58
#define A13 59
#define A14 60
#define A15 61
#endif
// ============================================================================
// Math constants
// ============================================================================
#ifndef PI
#define PI 3.14159265358979323846
#define HALF_PI 1.57079632679489661923
#define TWO_PI 6.28318530717958647692
#define DEG_TO_RAD 0.017453292519943295
#define RAD_TO_DEG 57.29577951308232
#endif
// ============================================================================
// Interrupt constants
// ============================================================================
#ifndef CHANGE
#define CHANGE 1
#define FALLING 2
#define RISING 3
#endif
// ============================================================================
// MockArduino -- global state for all Arduino functions
// ============================================================================
static const int MOCK_ARDUINO_MAX_PINS = 70; // Enough for Mega
struct MockArduinoState {
// Pin state
uint8_t pin_modes[MOCK_ARDUINO_MAX_PINS];
int pin_digital[MOCK_ARDUINO_MAX_PINS]; // digital values
int pin_analog[MOCK_ARDUINO_MAX_PINS]; // analog read values (0-1023)
int pin_pwm[MOCK_ARDUINO_MAX_PINS]; // analogWrite values (0-255)
// Timing
unsigned long millis_value;
unsigned long micros_value;
// Serial capture
std::string serial_output;
std::vector<uint8_t> serial_input;
unsigned long serial_baud;
bool serial_begun;
// Interrupt tracking
struct InterruptHandler {
void (*callback)();
int mode;
bool attached;
};
InterruptHandler interrupts[8]; // INT0-INT7
bool interrupts_enabled;
// GPIO event log
struct GpioEvent {
unsigned long timestamp;
uint8_t pin;
int value;
bool is_analog; // true = analogWrite, false = digitalWrite
};
std::vector<GpioEvent> gpio_log;
// Delay log (to verify timing behavior)
std::vector<unsigned long> delay_log;
};
// Global state -- tests access this directly
extern MockArduinoState _mock_arduino;
// ============================================================================
// Test control API -- call from tests to set up / inspect state
// ============================================================================
// Reset all state to defaults
void mock_arduino_reset();
// Clock control
void mock_arduino_advance_millis(unsigned long ms);
void mock_arduino_set_millis(unsigned long ms);
// Input injection
void mock_arduino_set_digital(uint8_t pin, int value);
void mock_arduino_set_analog(uint8_t pin, int value);
// Output inspection
int mock_arduino_get_digital(uint8_t pin);
int mock_arduino_get_pwm(uint8_t pin);
uint8_t mock_arduino_get_pin_mode(uint8_t pin);
// Serial inspection
const std::string& mock_arduino_serial_output();
void mock_arduino_inject_serial(const std::string& data);
void mock_arduino_clear_serial();
// GPIO log
const std::vector<MockArduinoState::GpioEvent>& mock_arduino_gpio_log();
void mock_arduino_clear_gpio_log();
int mock_arduino_count_writes(uint8_t pin, int value);
// ============================================================================
// Arduino core functions -- called by application code
// ============================================================================
// Digital I/O
void pinMode(uint8_t pin, uint8_t mode);
void digitalWrite(uint8_t pin, uint8_t val);
int digitalRead(uint8_t pin);
// Analog I/O
int analogRead(uint8_t pin);
void analogWrite(uint8_t pin, int val);
// Timing
unsigned long millis();
unsigned long micros();
void delay(unsigned long ms);
void delayMicroseconds(unsigned long us);
// Math helpers
long map(long value, long fromLow, long fromHigh, long toLow, long toHigh);
long constrain(long value, long low, long high);
long random(long max);
long random(long min, long max);
void randomSeed(unsigned long seed);
// Interrupts
void attachInterrupt(uint8_t interrupt, void (*callback)(), int mode);
void detachInterrupt(uint8_t interrupt);
void noInterrupts();
void interrupts();
uint8_t digitalPinToInterrupt(uint8_t pin);
// Bit manipulation (Arduino built-ins)
#ifndef bitRead
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, b) ((b) ? bitSet(value, bit) : bitClear(value, bit))
#define bit(n) (1UL << (n))
#define lowByte(w) ((uint8_t)((w) & 0xFF))
#define highByte(w) ((uint8_t)((w) >> 8))
#endif
// ============================================================================
// MockSerial -- replaces Serial global
// ============================================================================
class MockSerial {
public:
void begin(unsigned long baud);
void end();
size_t print(const char* str);
size_t print(char c);
size_t print(int val, int base = 10);
size_t print(unsigned int val, int base = 10);
size_t print(long val, int base = 10);
size_t print(unsigned long val, int base = 10);
size_t print(double val, int decimals = 2);
size_t print(const std::string& str);
size_t println(const char* str = "");
size_t println(char c);
size_t println(int val, int base = 10);
size_t println(unsigned int val, int base = 10);
size_t println(long val, int base = 10);
size_t println(unsigned long val, int base = 10);
size_t println(double val, int decimals = 2);
size_t println(const std::string& str);
size_t write(uint8_t b);
size_t write(const uint8_t* buf, size_t len);
int available();
int read();
int peek();
void flush();
explicit operator bool() const { return _mock_arduino.serial_begun; }
};
// Global Serial instance (matches Arduino's global)
extern MockSerial Serial;
// ============================================================================
// Minimal String class (Arduino-compatible subset)
// ============================================================================
class String {
public:
String() : data_() {}
String(const char* s) : data_(s ? s : "") {}
String(const std::string& s) : data_(s) {}
String(char c) : data_(1, c) {}
String(int val, int base = 10);
String(unsigned int val, int base = 10);
String(long val, int base = 10);
String(unsigned long val, int base = 10);
String(double val, int decimals = 2);
// Access
unsigned int length() const { return (unsigned int)data_.size(); }
const char* c_str() const { return data_.c_str(); }
char charAt(unsigned int index) const;
void setCharAt(unsigned int index, char c);
// Comparison
bool equals(const String& other) const { return data_ == other.data_; }
bool equalsIgnoreCase(const String& other) const;
int compareTo(const String& other) const;
// Search
int indexOf(char ch, unsigned int fromIndex = 0) const;
int indexOf(const String& str, unsigned int fromIndex = 0) const;
int lastIndexOf(char ch) const;
// Modification
String substring(unsigned int from, unsigned int to = 0xFFFFFFFF) const;
void toLowerCase();
void toUpperCase();
void trim();
void replace(const String& from, const String& to);
void remove(unsigned int index, unsigned int count = 1);
// Conversion
long toInt() const;
float toFloat() const;
double toDouble() const;
// Concatenation
String& concat(const String& other);
String& concat(const char* s);
String& concat(char c);
String& concat(int val);
String& concat(unsigned long val);
// Operators
String& operator+=(const String& rhs) { return concat(rhs); }
String& operator+=(const char* rhs) { return concat(rhs); }
String& operator+=(char rhs) { return concat(rhs); }
String operator+(const String& rhs) const;
bool operator==(const String& rhs) const { return data_ == rhs.data_; }
bool operator==(const char* rhs) const { return data_ == (rhs ? rhs : ""); }
bool operator!=(const String& rhs) const { return data_ != rhs.data_; }
bool operator<(const String& rhs) const { return data_ < rhs.data_; }
char operator[](unsigned int index) const;
char& operator[](unsigned int index);
// Interop with std::string
operator std::string() const { return data_; }
private:
std::string data_;
};
// ============================================================================
// Arduino.h compatibility alias
// ============================================================================
// So that #include <Arduino.h> resolves to this file via include path,
// we provide this comment as a note. The CMakeLists.txt adds the mocks/
// directory to the include path, so #include <Arduino.h> becomes:
// #include "mock_arduino.h" (via the Arduino.h shim in this directory)
#endif // MOCK_ARDUINO_H