Files
ota-reference-design/examples/simulations/ab-trial-boot/tests.cpp
T
2026-10-11 07:21:11 -04:00

125 lines
4.3 KiB
C++

#include "simulation.hpp"
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
namespace {
void expect(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template <class Operation>
void expect_rejected(Operation operation) {
try {
operation();
} catch (const std::logic_error&) {
return;
}
throw std::runtime_error("Invalid operation was accepted");
}
void scenario_outcomes() {
struct Expected {
const char* scenario;
ota::Phase phase;
ota::Slot normal;
const char* outcome;
};
const Expected expected[] = {
{"success", ota::Phase::BConfirmed, ota::Slot::B, "B confirmed"},
{"health-failure", ota::Phase::RecoveryChecked, ota::Slot::A, "Recovered to A"},
{"attempts-exhausted", ota::Phase::RecoveryChecked, ota::Slot::A, "Recovered to A"},
{"restart-during-trial", ota::Phase::BConfirmed, ota::Slot::B, "B confirmed"},
{"interrupted-preparation", ota::Phase::PreparingB, ota::Slot::A, ""},
{"recovery-failure", ota::Phase::SeparateRecoveryRequired, ota::Slot::A,
"Recovery A failed"}
};
for (const auto& result : expected) {
ota::TemporaryDirectory directory;
auto device = ota::run_scenario(result.scenario, directory.path());
const ota::Device restored(directory.path());
const auto& state = restored.state();
expect(state.phase == result.phase, "Unexpected saved phase");
expect(state.normal == result.normal, "Unexpected normal slot");
expect(state.outcome == result.outcome, "Unexpected saved outcome");
expect(state.a_ready, "The retained slot was modified");
if (state.phase == ota::Phase::SeparateRecoveryRequired) {
expect_rejected([&] { device.boot(); });
}
}
}
void saved_attempts() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare();
device.request_trial(2);
for (int left : {1, 0}) {
device = ota::Device(directory.path());
expect(device.boot() == ota::Slot::B, "Expected trial boot of B");
const ota::Device restored(directory.path());
expect(restored.state().attempts_left == left, "Attempt was not saved before boot");
expect(restored.state().normal == ota::Slot::A, "B was confirmed prematurely");
}
device = ota::Device(directory.path());
expect(device.boot() == ota::Slot::A, "Exhausted trial did not return to A");
expect(device.state().phase == ota::Phase::RecoveryRunning, "Recovery checks skipped");
}
void incomplete_slot() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare(false);
device = ota::Device(directory.path());
expect(!device.state().b_ready, "Incomplete B was marked ready");
expect_rejected([&] { device.request_trial(); });
expect(device.boot() == ota::Slot::A, "Incomplete B was selected");
}
void confirmation_requires_boot() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare();
expect_rejected([&] { device.request_trial(0); });
expect_rejected([&] { device.request_trial(-1); });
device.request_trial();
expect_rejected([&] { device.check(true); });
device.boot();
ota::Device restored(directory.path());
expect(!restored.running(), "Running slot survived reconstruction");
expect_rejected([&] { restored.check(true); });
device.check(true);
expect_rejected([&] { device.check(false); });
}
void unreadable_state() {
ota::TemporaryDirectory directory;
{
std::ofstream output(directory.path() / "boot-state.txt");
output << "incomplete record\n";
}
try {
const ota::Device device(directory.path());
} catch (const std::runtime_error&) {
return;
}
throw std::runtime_error("Unreadable state was accepted");
}
} // namespace
int main() {
try {
scenario_outcomes();
saved_attempts();
incomplete_slot();
confirmation_requires_boot();
unreadable_state();
std::cout << "All recovery invariant checks passed\n";
} catch (const std::exception& error) {
std::cerr << "Test failed: " << error.what() << '\n';
return 1;
}
}