diagrams and simulation
This commit is contained in:
1 parent
bf9871eb9e
commit
d65910a7c5
18 files changed
+926
-4
No files matched your search
@@ -0,0 +1,23 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(ab_trial_boot LANGUAGES CXX)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
add_library(ab-simulation simulation.cpp)
|
||||
target_compile_features(ab-simulation PUBLIC cxx_std_17)
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(ab-simulation PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
add_executable(ab-simulate main.cpp)
|
||||
target_link_libraries(ab-simulate PRIVATE ab-simulation)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(ab-simulation-tests tests.cpp)
|
||||
target_link_libraries(ab-simulation-tests PRIVATE ab-simulation)
|
||||
add_test(NAME recovery-invariants COMMAND ab-simulation-tests)
|
||||
add_test(NAME all-scenarios COMMAND ab-simulate all)
|
||||
add_test(NAME cli-help COMMAND ab-simulate --help)
|
||||
add_test(NAME cli-invalid-scenario COMMAND ab-simulate invalid)
|
||||
set_tests_properties(cli-invalid-scenario PROPERTIES WILL_FAIL TRUE)
|
||||
endif()
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "simulation.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
const std::string selected = argc > 1 ? argv[1] : "all";
|
||||
if (argc > 2 || selected == "--help" || selected == "-h" ||
|
||||
(selected != "all" && std::find(ota::scenarios().begin(), ota::scenarios().end(),
|
||||
selected) == ota::scenarios().end())) {
|
||||
std::ostream& output = (argc == 2 && (selected == "--help" || selected == "-h"))
|
||||
? std::cout : std::cerr;
|
||||
output << "Usage: ab-simulate [all";
|
||||
for (const auto& scenario : ota::scenarios()) output << '|' << scenario;
|
||||
output << "]\n";
|
||||
return &output == &std::cout ? 0 : 2;
|
||||
}
|
||||
try {
|
||||
for (const auto& scenario : ota::scenarios()) {
|
||||
if (selected != "all" && selected != scenario) continue;
|
||||
std::cout << "\n=== " << scenario << " ===\n";
|
||||
ota::TemporaryDirectory directory;
|
||||
ota::run_scenario(scenario, directory.path(), &std::cout);
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "Simulation failed: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# A/B trial boot simulation
|
||||
|
||||
This C++17 standard-library example follows the [trial boot diagram](../../../diagrams/plantuml/ab-trial-boot.puml), [chapter 04](../../../docs/04-starting-an-updated-system-or-service.md), and [ADR-014](../../../adr/ADR-014-trial-boot-and-confirmation.md). It illustrates boot decisions; it does not install software or access a device. No third-party libraries are required.
|
||||
|
||||
Build and run from the repository root with CMake 3.20 or later and a C++17 compiler. These commands use a Linux temporary build directory to keep generated files outside the repository:
|
||||
|
||||
```sh
|
||||
cmake -S examples/simulations/ab-trial-boot -B /tmp/ota-ab-build -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build /tmp/ota-ab-build
|
||||
/tmp/ota-ab-build/ab-simulate all
|
||||
/tmp/ota-ab-build/ab-simulate attempts-exhausted
|
||||
ctest --test-dir /tmp/ota-ab-build --output-on-failure
|
||||
```
|
||||
|
||||
| Scenario | Expected result |
|
||||
|---|---|
|
||||
| `success` | Checks pass; B is confirmed and selected on the next boot. |
|
||||
| `health-failure` | B fails checks; reset selects A; recovery checks pass. |
|
||||
| `attempts-exhausted` | B never reaches checks; two unconfirmed attempts lead to A. |
|
||||
| `restart-during-trial` | A reset preserves the consumed attempt; the second trial passes. |
|
||||
| `interrupted-preparation` | Incomplete B remains ineligible; reset selects A. |
|
||||
| `recovery-failure` | A also fails; stop at the boundary of a separate recovery policy. |
|
||||
|
||||
With no argument, `ab-simulate` runs all scenarios. Use `--help` to list the accepted arguments. Invalid arguments return a nonzero exit status.
|
||||
|
||||
The attempt limit of two is an illustrative choice. Each reset reconstructs the device from a versioned text record (`boot-state.txt`) in a temporary directory. The running slot is volatile. The saved normal slot stays A until B is confirmed. Boot control saves each consumed attempt before returning B as the selected system. A failed health check requests recovery immediately rather than spending the remaining trial attempts.
|
||||
|
||||
The example keeps boot state outside the simulated slots, as shown in the [storage diagram](../../../diagrams/plantuml/ab-storage-layout.puml). Each scenario uses a separate temporary directory, removed when it finishes. The model covers one A-to-B update; it does not model subsequent releases.
|
||||
|
||||
## Assumptions and limits
|
||||
|
||||
Installation and artifact verification are represented by one preparation operation. Health checks are supplied boolean outcomes, including a timeout represented as failure. Resets are explicit calls; watchdog coverage and real startup timing are not simulated. Recovery assumes that A and shared data remain usable, unless the recovery-failure scenario supplies a failed check.
|
||||
|
||||
The text file is a teaching stand-in for boot state, using standard C++ streams and `std::filesystem`. It records the phase, normal slot, optional trial slot, attempts remaining, slot readiness, and outcome. Replacement uses a temporary file and rename, but does not flush storage, implement redundant records, or recover invalid records. Unreadable records stop the simulation with an error; they are never treated as confirmation. This is not a power-loss-safe boot-state implementation. The simulation assumes completed valid writes; it does not test interruption within a write, coordination with OTA progress, data migration, signatures, bootloader integration, or hardware failure. These need platform-specific implementation and failure-injection tests.
|
||||
|
||||
Outcomes remain local in the model. There is no server dependency or reporting transport. A real implementation must retain pending reports separately and send them when connectivity returns.
|
||||
|
||||
## Source and checks
|
||||
|
||||
- [simulation.hpp](simulation.hpp) and [simulation.cpp](simulation.cpp): typed state model, saved records, reset behavior, and scenarios.
|
||||
- [main.cpp](main.cpp): command-line runner.
|
||||
- [tests.cpp](tests.cpp): scenario outcomes and invariants for saved attempts, incomplete slots, confirmation, and unreadable state. Checks remain enabled in Release builds.
|
||||
- [CMakeLists.txt](CMakeLists.txt): builds the runner and registers the invariant and CLI checks with CTest.
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "simulation.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace ota {
|
||||
namespace {
|
||||
constexpr const char* phase_names[] = {
|
||||
"A confirmed", "Prepare B", "B prepared and verified", "B trial pending",
|
||||
"B trial running", "B confirmed", "Recovery to A pending",
|
||||
"A recovery running", "A recovery checked", "Separate recovery required"
|
||||
};
|
||||
|
||||
Phase read_phase(const std::string& value) {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (value == phase_names[i]) return static_cast<Phase>(i);
|
||||
}
|
||||
throw std::runtime_error("Invalid saved phase");
|
||||
}
|
||||
|
||||
Slot read_slot(const std::string& value) {
|
||||
if (value == "A") return Slot::A;
|
||||
if (value == "B") return Slot::B;
|
||||
throw std::runtime_error("Invalid saved slot");
|
||||
}
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) throw std::logic_error(message);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const char* name(Phase phase) { return phase_names[static_cast<int>(phase)]; }
|
||||
const char* name(Slot slot) { return slot == Slot::A ? "A" : "B"; }
|
||||
|
||||
Device::Device(const std::filesystem::path& directory)
|
||||
: path_(directory / "boot-state.txt") {
|
||||
if (!std::filesystem::exists(path_)) {
|
||||
save();
|
||||
return;
|
||||
}
|
||||
std::ifstream input(path_);
|
||||
std::string header, phase, normal, trial;
|
||||
int a_ready = 0, b_ready = 0;
|
||||
if (!(input >> header >> std::quoted(phase) >> normal >> trial
|
||||
>> state_.attempts_left >> a_ready >> b_ready
|
||||
>> std::quoted(state_.outcome)) || header != "OTA_AB_STATE_V1") {
|
||||
throw std::runtime_error("Unreadable saved boot state");
|
||||
}
|
||||
input >> std::ws;
|
||||
if (!input.eof() || state_.attempts_left < 0 ||
|
||||
(a_ready != 0 && a_ready != 1) || (b_ready != 0 && b_ready != 1)) {
|
||||
throw std::runtime_error("Invalid saved boot state");
|
||||
}
|
||||
state_.phase = read_phase(phase);
|
||||
state_.normal = read_slot(normal);
|
||||
if (trial != "none") state_.trial = read_slot(trial);
|
||||
state_.a_ready = a_ready != 0;
|
||||
state_.b_ready = b_ready != 0;
|
||||
}
|
||||
|
||||
void Device::save() const {
|
||||
auto temporary = path_;
|
||||
temporary += ".tmp";
|
||||
std::ofstream output(temporary, std::ios::trunc);
|
||||
output << "OTA_AB_STATE_V1\n" << std::quoted(name(state_.phase)) << '\n'
|
||||
<< name(state_.normal) << '\n'
|
||||
<< (state_.trial ? name(*state_.trial) : "none") << '\n'
|
||||
<< state_.attempts_left << '\n'
|
||||
<< state_.a_ready << ' ' << state_.b_ready << '\n'
|
||||
<< std::quoted(state_.outcome) << '\n';
|
||||
output.close();
|
||||
if (!output) throw std::runtime_error("Cannot save boot state");
|
||||
// Teaching stand-in: rename alone does not guarantee power-loss durability.
|
||||
std::filesystem::rename(temporary, path_);
|
||||
}
|
||||
|
||||
void Device::prepare(bool complete) {
|
||||
require(state_.phase == Phase::AConfirmed || state_.phase == Phase::PreparingB,
|
||||
"Cannot prepare B in this phase");
|
||||
state_.b_ready = false;
|
||||
state_.phase = Phase::PreparingB;
|
||||
save();
|
||||
if (complete) {
|
||||
// Represents complete installation AND artifact verification.
|
||||
state_.b_ready = true;
|
||||
state_.phase = Phase::BPrepared;
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
void Device::request_trial(int attempts) {
|
||||
require(state_.phase == Phase::BPrepared && state_.b_ready,
|
||||
"B must be prepared and verified before trial");
|
||||
require(attempts > 0, "The trial needs at least one attempt");
|
||||
state_.phase = Phase::TrialPending;
|
||||
state_.trial = Slot::B;
|
||||
state_.attempts_left = attempts;
|
||||
save();
|
||||
}
|
||||
|
||||
Slot Device::boot() {
|
||||
require(state_.phase != Phase::SeparateRecoveryRequired,
|
||||
"Separate recovery is required; do not alternate failed systems");
|
||||
if (state_.trial) {
|
||||
if (state_.attempts_left > 0) {
|
||||
--state_.attempts_left;
|
||||
state_.phase = Phase::TrialRunning;
|
||||
// Persist the consumed attempt BEFORE handing control to B.
|
||||
save();
|
||||
running_ = *state_.trial;
|
||||
return *running_;
|
||||
}
|
||||
state_.phase = Phase::RecoveryPending;
|
||||
state_.trial.reset();
|
||||
save();
|
||||
}
|
||||
if (state_.phase == Phase::RecoveryPending) {
|
||||
state_.phase = Phase::RecoveryRunning;
|
||||
save();
|
||||
}
|
||||
running_ = state_.normal;
|
||||
return *running_;
|
||||
}
|
||||
|
||||
void Device::check(bool healthy) {
|
||||
require(running_.has_value(), "Boot before checking the running system");
|
||||
require(state_.phase == Phase::TrialRunning || state_.phase == Phase::RecoveryRunning,
|
||||
"Checks require a running trial or recovery");
|
||||
if (state_.phase == Phase::TrialRunning) {
|
||||
state_.trial.reset();
|
||||
state_.attempts_left = 0;
|
||||
if (healthy) {
|
||||
state_.phase = Phase::BConfirmed;
|
||||
state_.normal = Slot::B;
|
||||
state_.outcome = "B confirmed";
|
||||
} else {
|
||||
state_.phase = Phase::RecoveryPending;
|
||||
}
|
||||
} else if (healthy) {
|
||||
state_.phase = Phase::RecoveryChecked;
|
||||
state_.outcome = "Recovered to A";
|
||||
} else {
|
||||
state_.phase = Phase::SeparateRecoveryRequired;
|
||||
state_.outcome = "Recovery A failed";
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
TemporaryDirectory::TemporaryDirectory() {
|
||||
std::random_device random;
|
||||
for (int attempt = 0; attempt < 100; ++attempt) {
|
||||
auto candidate = std::filesystem::temp_directory_path() /
|
||||
("ota-ab-simulation-" + std::to_string(random()) + "-" + std::to_string(random()));
|
||||
if (std::filesystem::create_directory(candidate)) {
|
||||
path_ = std::move(candidate);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("Cannot create a unique temporary directory");
|
||||
}
|
||||
|
||||
TemporaryDirectory::~TemporaryDirectory() {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove_all(path_, ignored);
|
||||
}
|
||||
|
||||
const std::vector<std::string>& scenarios() {
|
||||
static const std::vector<std::string> names = {
|
||||
"success", "health-failure", "attempts-exhausted", "restart-during-trial",
|
||||
"interrupted-preparation", "recovery-failure"
|
||||
};
|
||||
return names;
|
||||
}
|
||||
|
||||
Device run_scenario(const std::string& scenario, const std::filesystem::path& directory,
|
||||
std::ostream* output) {
|
||||
if (std::find(scenarios().begin(), scenarios().end(), scenario) == scenarios().end()) {
|
||||
throw std::invalid_argument("Unknown scenario: " + scenario);
|
||||
}
|
||||
Device device(directory);
|
||||
auto show = [&](const std::string& event) {
|
||||
if (!output) return;
|
||||
const auto& state = device.state();
|
||||
*output << event << ": " << name(state.phase) << "; normal=" << name(state.normal)
|
||||
<< "; attempts_left=" << state.attempts_left << "; running="
|
||||
<< (device.running() ? name(*device.running()) : "none") << '\n';
|
||||
};
|
||||
auto reset = [&] {
|
||||
device = Device(directory);
|
||||
const auto selected = device.boot();
|
||||
show(std::string("Reset -> ") + name(selected));
|
||||
};
|
||||
auto message = [&](const char* text) { if (output) *output << text << '\n'; };
|
||||
|
||||
show("Initial state");
|
||||
if (scenario == "interrupted-preparation") {
|
||||
device.prepare(false);
|
||||
show("Interrupted installation: B remains ineligible");
|
||||
reset();
|
||||
return device;
|
||||
}
|
||||
device.prepare();
|
||||
show("Installation and verification complete");
|
||||
device.request_trial(2);
|
||||
show("Trial requested");
|
||||
reset();
|
||||
if (scenario == "attempts-exhausted" || scenario == "restart-during-trial") {
|
||||
message("No confirmation: simulate a reset caused by power loss or watchdog");
|
||||
reset();
|
||||
}
|
||||
if (scenario == "attempts-exhausted") {
|
||||
message("B still cannot reach health checks; simulate another reset");
|
||||
reset();
|
||||
device.check(true);
|
||||
show("Recovery checks pass");
|
||||
} else if (scenario == "health-failure" || scenario == "recovery-failure") {
|
||||
device.check(false);
|
||||
show("B health checks fail");
|
||||
reset();
|
||||
device.check(scenario != "recovery-failure");
|
||||
show("Recovery checks complete");
|
||||
} else {
|
||||
device.check(true);
|
||||
show("B health checks pass; confirmation saved");
|
||||
reset();
|
||||
}
|
||||
return device;
|
||||
}
|
||||
} // namespace ota
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ota {
|
||||
|
||||
enum class Phase {
|
||||
AConfirmed, PreparingB, BPrepared, TrialPending, TrialRunning,
|
||||
BConfirmed, RecoveryPending, RecoveryRunning, RecoveryChecked,
|
||||
SeparateRecoveryRequired
|
||||
};
|
||||
|
||||
enum class Slot { A, B };
|
||||
|
||||
const char* name(Phase phase);
|
||||
const char* name(Slot slot);
|
||||
|
||||
struct State {
|
||||
Phase phase = Phase::AConfirmed;
|
||||
Slot normal = Slot::A;
|
||||
std::optional<Slot> trial;
|
||||
int attempts_left = 0;
|
||||
bool a_ready = true;
|
||||
bool b_ready = false;
|
||||
std::string outcome;
|
||||
};
|
||||
|
||||
class Device {
|
||||
public:
|
||||
explicit Device(const std::filesystem::path& directory);
|
||||
const State& state() const { return state_; }
|
||||
std::optional<Slot> running() const { return running_; }
|
||||
void prepare(bool complete = true);
|
||||
void request_trial(int attempts = 2);
|
||||
Slot boot();
|
||||
void check(bool healthy);
|
||||
|
||||
private:
|
||||
void save() const;
|
||||
State state_;
|
||||
std::filesystem::path path_;
|
||||
// Volatile: reconstructing a Device requires another boot before checks.
|
||||
std::optional<Slot> running_;
|
||||
};
|
||||
|
||||
// Own only a freshly created directory and clean it up at scope exit.
|
||||
class TemporaryDirectory {
|
||||
public:
|
||||
TemporaryDirectory();
|
||||
~TemporaryDirectory();
|
||||
TemporaryDirectory(const TemporaryDirectory&) = delete;
|
||||
TemporaryDirectory& operator=(const TemporaryDirectory&) = delete;
|
||||
const std::filesystem::path& path() const { return path_; }
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
const std::vector<std::string>& scenarios();
|
||||
Device run_scenario(const std::string& scenario,
|
||||
const std::filesystem::path& directory,
|
||||
std::ostream* output = nullptr);
|
||||
|
||||
} // namespace ota
|
||||
@@ -0,0 +1,124 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user