162 lines
7.6 KiB
C++
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;
|
|
}
|
|
}
|