#include "simulation.hpp" #include #include #include #include namespace { void expect(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } template 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; } }