next chapter
This commit is contained in:
1 parent
9385583955
commit
c98eccc057
21 files changed
+1054
-11
No files matched your search
@@ -0,0 +1,161 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user