125 lines
4.3 KiB
C++
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;
|
|
}
|
|
}
|