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