Files
2026-10-25 08:31:08 -04:00

162 lines
7.6 KiB
C++

#include "simulation.hpp"
#include <iostream>
#include <stdexcept>
namespace model = ota::rollback;
namespace {
void expect(bool condition, const char *message) {
if (!condition) throw std::runtime_error(message);
}
template<class Operation>
void rejected(Operation operation) {
try {
operation();
} catch (const std::logic_error &) {
return;
}
throw std::runtime_error("Invalid operation accepted");
}
void finish(model::Storage &storage) {
model::Coordinator coordinator(storage);
for (int steps = 0; !model::terminal(storage.phase); ++steps) {
expect(steps < 16, "Recovery did not terminate");
coordinator.advance();
}
}
void scenario_outcomes() {
for (const auto &scenario: model::scenarios()) {
const auto storage = model::run_scenario(scenario);
const bool recovered = scenario == "success" || scenario == "backup-restore" ||
scenario == "restart-during-recovery" || scenario == "blocked-release";
expect(storage.phase == (recovered ? model::Phase::Recovered : model::Phase::SeparateRecovery), "Wrong scenario outcome");
expect(storage.blocked_releases.count("system-2") == 1, "Failed release block lost");
expect(storage.report_pending && !storage.failure_reason.empty() && !storage.recovery_reason.empty(), "Recovery diagnosis lost");
expect(storage.attempts_left >= 0 && storage.boots_requested <= 2, "Boot budget violated");
if (recovered) {
expect(storage.running == model::Slot::A && storage.data.schema == 1, "Recovered without usable A and data");
expect(model::can_offer(storage, "system-3"), "Different release cannot reach normal admission");
}
expect(!model::can_offer(storage, "system-2"), "Failed release automatically retried");
}
const auto restored = model::run_scenario("backup-restore");
expect(restored.data.records == std::vector<int>({1, 2}) && restored.records_lost == 2 && restored.restores == 1, "Restore data loss not accounted");
const auto incompatible = model::run_scenario("incompatible-data");
expect(incompatible.data.schema == 2 && incompatible.data.records.size() == 4 && incompatible.boots_requested == 0, "Unsafe rollback changed data or booted A");
const auto exhausted = model::run_scenario("attempts-exhausted");
expect(exhausted.attempts_left == 0 && exhausted.boots_requested == 2, "Reset refreshed recovery attempts");
}
void reset_boundaries() {
// Cut after intent, assessment, restore, boot handoff, and completed checks.
for (int cut = 0; cut <= 4; ++cut) {
model::Storage storage;
storage.data.schema = 2;
storage.backup = model::Data{1, {1, 2}};
storage.backup_valid = true;
storage.restore_authorized = true;
{
model::Coordinator coordinator(storage);
coordinator.start("trial health failure");
for (int step = 0; step < cut; ++step) coordinator.advance();
}
const int consumed = storage.boots_requested;
model::Coordinator restarted(storage);
restarted.after_reset();
expect(storage.boots_requested == consumed && storage.attempts_left == 2 - consumed, "Reset changed saved budget");
rejected([&] { restarted.start("rediscovered failure"); });
finish(storage);
expect(storage.phase == model::Phase::Recovered, "Reset prevented recovery");
expect(storage.restores == 1 && storage.records_lost == 2, "Reset repeated backup restore");
// Data acquired after recovery must not be replaced by the old backup.
storage.data.records.push_back(5);
model::Coordinator next(storage);
next.after_reset();
next.advance();
expect(storage.data.records == std::vector<int>({1, 2, 5}) && storage.restores == 1, "Completed recovery restored old backup again");
}
}
void admission_and_guards() {
model::Storage storage;
model::Coordinator coordinator(storage);
rejected([&] { coordinator.advance(); });
rejected([&] { coordinator.start(""); });
coordinator.start("local health failure");
expect(storage.blocked_releases.count(storage.failed_release) == 1 && storage.boots_requested == 0, "Recovery acted before saving block");
expect(!model::can_offer(storage, "system-3"), "Competing update admitted during recovery");
coordinator.advance();
coordinator.advance();
expect(storage.phase == model::Phase::CheckPrevious, "Boot handoff reported success before checks");
model::Coordinator process_restart(storage);
process_restart.advance(false);
expect(storage.phase == model::Phase::SeparateRecovery && storage.boots_requested == 1, "Failed recovery did not stop");
process_restart.after_reset();
process_restart.advance();
expect(storage.phase == model::Phase::SeparateRecovery && storage.boots_requested == 1, "Terminal failure started another boot");
}
void restore_guards() {
for (int fault = 0; fault < 4; ++fault) {
model::Storage storage;
storage.data.schema = 2;
storage.backup = model::Data{1, {1, 2}};
storage.restore_authorized = true;
storage.backup_valid = true;
if (fault == 0) storage.restore_authorized = false;
if (fault == 1) storage.backup_valid = false;
if (fault == 2) storage.backup->schema = 2;
if (fault == 3) storage.backup.reset();
model::Coordinator coordinator(storage);
coordinator.start("trial failed after data change");
finish(storage);
expect(storage.phase == model::Phase::SeparateRecovery && storage.restores == 0 && storage.boots_requested == 0, "Invalid restore accepted");
expect(storage.data.schema == 2 && storage.data.records.size() == 4, "Rejected restore changed live data");
}
model::Storage storage;
storage.data.schema = 2;
storage.backup = model::Data{1, {1, 2}};
storage.backup_valid = storage.restore_authorized = true;
model::Coordinator coordinator(storage);
coordinator.start("trial failed");
coordinator.advance();
storage.restore_authorized = false;
coordinator.advance();
expect(storage.phase == model::Phase::SeparateRecovery && storage.restores == 0, "Restore ignored revoked permission");
}
void target_changes() {
for (bool change_data: {false, true}) {
model::Storage storage;
model::Coordinator coordinator(storage);
coordinator.start("trial failed");
coordinator.advance();
if (change_data) storage.data.schema = 2;
else
storage.window_open = false;
coordinator.advance();
expect(storage.phase == model::Phase::SeparateRecovery && storage.boots_requested == 0, "Changed prerequisites allowed boot handoff");
}
}
}// namespace
int main() {
try {
scenario_outcomes();
reset_boundaries();
admission_and_guards();
restore_guards();
target_changes();
std::cout << "All rollback policy checks passed\n";
} catch (const std::exception &error) {
std::cerr << "Test failed: " << error.what() << '\n';
return 1;
}
}