Layer 3: Templates as pure data, weather template, .anvilignore refresh system

Templates are now composed declaratively via template.toml -- no Rust code
changes needed to add new templates. The weather station is the first
composed template, demonstrating the full pattern.

Template engine:
- Composed templates declare base, required libraries, and per-board pins
- Overlay mechanism replaces base files (app, sketch, tests) cleanly
- Generic orchestration: extract base, apply overlay, install libs, assign pins
- Template name tracked in .anvil.toml for refresh awareness

Weather template (--template weather):
- WeatherApp with 2-second polling, C/F conversion, serial output
- TMP36 driver: TempSensor interface, Tmp36 impl, Tmp36Mock, Tmp36Sim
- Managed example tests in test_weather.cpp (unit + system)
- Minimal student starters in test_unit.cpp and test_system.cpp
- Per-board pin defaults (A0 for uno, A0 for mega, A0 for nano)

.anvilignore system:
- Glob pattern matching (*, ?) with comments and backslash normalization
- Default patterns protect student tests, app code, sketch, config
- anvil refresh --force respects .anvilignore
- anvil refresh --force --file <path> overrides ignore for one file
- anvil refresh --ignore/--unignore manages patterns from CLI
- Missing managed files always recreated even without --force
- .anvilignore itself is in NEVER_REFRESH (cannot be overwritten)

Refresh rewrite:
- Discovers all template-produced files dynamically (no hardcoded list)
- Extracts fresh template + libraries into temp dir for byte comparison
- Config template field drives which files are managed
- Separated missing-file creation from changed-file updates

428 tests passing on Windows MSVC, 0 warnings.
This commit is contained in:
Eric Ratliff
2026-02-21 20:52:48 -06:00
parent 0abe907811
commit ca855dd3af
17 changed files with 5190 additions and 236 deletions

View File

@@ -0,0 +1,28 @@
/*
* test_system.cpp -- Your system tests go here.
*
* This file is YOURS. Anvil will never overwrite it.
* The weather station example tests are in test_weather.cpp.
*
* System tests use SimHal and Tmp36Sim to exercise real application
* logic against simulated hardware. See test_weather.cpp for examples.
*/
#include <gtest/gtest.h>
#include "mock_arduino.h"
#include "hal.h"
#include "sim_hal.h"
#include "tmp36_sim.h"
#include "{{PROJECT_NAME}}_app.h"
// Example: add your own system tests below
// TEST(MySystemTests, DescribeWhatItTests) {
// mock_arduino_reset();
// SimHal sim;
// Tmp36Sim sensor(25.0f, 0.5f);
//
// WeatherApp app(&sim, &sensor);
// app.begin();
// // ... your test logic ...
// }

View File

@@ -0,0 +1,30 @@
/*
* test_unit.cpp -- Your unit tests go here.
*
* This file is YOURS. Anvil will never overwrite it.
* The weather station example tests are in test_weather.cpp.
*
* Unit tests use MockHal and Tmp36Mock to verify exact behavior
* without real hardware. See test_weather.cpp for examples.
*/
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "hal.h"
#include "mock_hal.h"
#include "tmp36_mock.h"
#include "{{PROJECT_NAME}}_app.h"
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::Return;
// Example: add your own tests below
// TEST(MyTests, DescribeWhatItTests) {
// ::testing::NiceMock<MockHal> mock;
// Tmp36Mock sensor;
// sensor.setTemperature(25.0f);
//
// // ... your test logic ...
// }

View File

@@ -0,0 +1,250 @@
/*
* test_weather.cpp -- Weather station example tests.
*
* THIS FILE IS MANAGED BY ANVIL and will be updated by `anvil refresh`.
* Do not edit -- put your own tests in test_unit.cpp and test_system.cpp.
*/
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "mock_arduino.h"
#include "hal.h"
#include "mock_hal.h"
#include "sim_hal.h"
#include "tmp36_mock.h"
#include "tmp36_sim.h"
#include "{{PROJECT_NAME}}_app.h"
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::Return;
using ::testing::HasSubstr;
// ============================================================================
// Unit Tests -- verify WeatherApp behavior with mock sensor
// ============================================================================
class WeatherUnitTest : public ::testing::Test {
protected:
void SetUp() override {
ON_CALL(mock_, millis()).WillByDefault(Return(0));
EXPECT_CALL(mock_, serialBegin(_)).Times(AnyNumber());
EXPECT_CALL(mock_, serialPrint(_)).Times(AnyNumber());
EXPECT_CALL(mock_, serialPrintln(_)).Times(AnyNumber());
EXPECT_CALL(mock_, millis()).Times(AnyNumber());
}
::testing::NiceMock<MockHal> mock_;
Tmp36Mock sensor_;
};
TEST_F(WeatherUnitTest, BeginPrintsStartupMessage) {
WeatherApp app(&mock_, &sensor_);
EXPECT_CALL(mock_, serialBegin(115200)).Times(1);
EXPECT_CALL(mock_, serialPrintln(HasSubstr("WeatherApp started"))).Times(1);
app.begin();
}
TEST_F(WeatherUnitTest, BeginTakesInitialReading) {
sensor_.setTemperature(25.0f);
WeatherApp app(&mock_, &sensor_);
app.begin();
EXPECT_EQ(app.readCount(), 1);
EXPECT_NEAR(app.lastCelsius(), 25.0f, 0.1f);
}
TEST_F(WeatherUnitTest, ReadsAfterInterval) {
sensor_.setTemperature(20.0f);
WeatherApp app(&mock_, &sensor_);
ON_CALL(mock_, millis()).WillByDefault(Return(0));
app.begin();
EXPECT_EQ(app.readCount(), 1);
// Not enough time yet
ON_CALL(mock_, millis()).WillByDefault(Return(1999));
app.update();
EXPECT_EQ(app.readCount(), 1);
// Now 2 seconds have passed
ON_CALL(mock_, millis()).WillByDefault(Return(2000));
app.update();
EXPECT_EQ(app.readCount(), 2);
}
TEST_F(WeatherUnitTest, DoesNotReadTooEarly) {
sensor_.setTemperature(22.0f);
WeatherApp app(&mock_, &sensor_);
ON_CALL(mock_, millis()).WillByDefault(Return(0));
app.begin();
ON_CALL(mock_, millis()).WillByDefault(Return(1500));
app.update();
EXPECT_EQ(app.readCount(), 1);
}
TEST_F(WeatherUnitTest, CelsiusToFahrenheitConversion) {
sensor_.setTemperature(0.0f);
WeatherApp app(&mock_, &sensor_);
app.begin();
EXPECT_NEAR(app.lastCelsius(), 0.0f, 0.1f);
EXPECT_NEAR(app.lastFahrenheit(), 32.0f, 0.1f);
}
TEST_F(WeatherUnitTest, BoilingPoint) {
sensor_.setTemperature(100.0f);
WeatherApp app(&mock_, &sensor_);
app.begin();
EXPECT_NEAR(app.lastCelsius(), 100.0f, 0.1f);
EXPECT_NEAR(app.lastFahrenheit(), 212.0f, 0.1f);
}
TEST_F(WeatherUnitTest, NegativeTemperature) {
sensor_.setTemperature(-10.0f);
WeatherApp app(&mock_, &sensor_);
app.begin();
EXPECT_NEAR(app.lastCelsius(), -10.0f, 0.1f);
EXPECT_NEAR(app.lastFahrenheit(), 14.0f, 0.1f);
}
TEST_F(WeatherUnitTest, PrintsTemperatureOnRead) {
sensor_.setTemperature(25.0f);
WeatherApp app(&mock_, &sensor_);
EXPECT_CALL(mock_, serialPrint(HasSubstr("Temperature: "))).Times(1);
EXPECT_CALL(mock_, serialPrintln(HasSubstr(" F)"))).Times(1);
app.begin();
}
TEST_F(WeatherUnitTest, MultipleReadingsTrackNewTemperature) {
WeatherApp app(&mock_, &sensor_);
sensor_.setTemperature(20.0f);
ON_CALL(mock_, millis()).WillByDefault(Return(0));
app.begin();
EXPECT_NEAR(app.lastCelsius(), 20.0f, 0.1f);
sensor_.setTemperature(30.0f);
ON_CALL(mock_, millis()).WillByDefault(Return(2000));
app.update();
EXPECT_NEAR(app.lastCelsius(), 30.0f, 0.1f);
EXPECT_EQ(app.readCount(), 2);
}
// ============================================================================
// System Tests -- exercise WeatherApp with simulated sensor and hardware
// ============================================================================
class WeatherSystemTest : public ::testing::Test {
protected:
void SetUp() override {
mock_arduino_reset();
sim_.setMillis(0);
}
SimHal sim_;
Tmp36Sim sensor_{25.0f}; // 25 C base temperature
};
TEST_F(WeatherSystemTest, StartsAndPrintsToSerial) {
WeatherApp app(&sim_, &sensor_);
app.begin();
std::string output = sim_.serialOutput();
EXPECT_NE(output.find("WeatherApp started"), std::string::npos);
EXPECT_NE(output.find("Temperature:"), std::string::npos);
}
TEST_F(WeatherSystemTest, InitialReadingIsReasonable) {
Tmp36Sim exact_sensor(25.0f, 0.0f); // zero noise
WeatherApp app(&sim_, &exact_sensor);
app.begin();
EXPECT_NEAR(app.lastCelsius(), 25.0f, 1.0f);
EXPECT_EQ(app.readCount(), 1);
}
TEST_F(WeatherSystemTest, ReadsAtTwoSecondIntervals) {
WeatherApp app(&sim_, &sensor_);
app.begin();
EXPECT_EQ(app.readCount(), 1);
// 1 second -- no new reading
sim_.advanceMillis(1000);
app.update();
EXPECT_EQ(app.readCount(), 1);
// 2 seconds -- new reading
sim_.advanceMillis(1000);
app.update();
EXPECT_EQ(app.readCount(), 2);
// 4 seconds -- another reading
sim_.advanceMillis(2000);
app.update();
EXPECT_EQ(app.readCount(), 3);
}
TEST_F(WeatherSystemTest, FiveMinuteRun) {
WeatherApp app(&sim_, &sensor_);
app.begin();
// Run 5 minutes at 100ms resolution
for (int i = 0; i < 3000; ++i) {
sim_.advanceMillis(100);
app.update();
}
// 5 minutes = 300 seconds / 2 second interval = 150 readings + 1 initial
EXPECT_EQ(app.readCount(), 151);
}
TEST_F(WeatherSystemTest, TemperatureChangeMidRun) {
Tmp36Sim sensor(20.0f, 0.0f); // start at 20 C, no noise
WeatherApp app(&sim_, &sensor);
app.begin();
EXPECT_NEAR(app.lastCelsius(), 20.0f, 1.0f);
// Change temperature and wait for next reading
sensor.setBaseTemperature(35.0f);
sim_.advanceMillis(2000);
app.update();
EXPECT_NEAR(app.lastCelsius(), 35.0f, 1.0f);
}
TEST_F(WeatherSystemTest, SerialOutputContainsFahrenheit) {
Tmp36Sim exact_sensor(0.0f, 0.0f); // 0 C = 32 F
WeatherApp app(&sim_, &exact_sensor);
app.begin();
std::string output = sim_.serialOutput();
EXPECT_NE(output.find("32"), std::string::npos)
<< "Should contain 32 F for 0 C: " << output;
}
TEST_F(WeatherSystemTest, NoisyReadingsStayInRange) {
Tmp36Sim noisy_sensor(25.0f, 2.0f); // +/- 2 C noise
noisy_sensor.setSeed(42);
WeatherApp app(&sim_, &noisy_sensor);
for (int i = 0; i < 20; ++i) {
sim_.setMillis(i * 2000);
if (i == 0) app.begin(); else app.update();
float c = app.lastCelsius();
EXPECT_GE(c, 20.0f) << "Reading " << i << " too low: " << c;
EXPECT_LE(c, 30.0f) << "Reading " << i << " too high: " << c;
}
}