06 - atomic update
This commit is contained in:
1 parent
6d63457737
commit
2bdacaf37a
21 files changed
+1302
-365
No files matched your search
@@ -4,26 +4,27 @@
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
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;
|
||||
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;
|
||||
for (const auto &scenario: ota::scenarios()) output << '|' << scenario;
|
||||
output << "]\n";
|
||||
return &output == &std::cout ? 0 : 2;
|
||||
}
|
||||
try {
|
||||
for (const auto& scenario : ota::scenarios()) {
|
||||
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) {
|
||||
} catch (const std::exception &error) {
|
||||
std::cerr << "Simulation failed: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -8,226 +8,223 @@
|
||||
#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"
|
||||
};
|
||||
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");
|
||||
}
|
||||
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");
|
||||
}
|
||||
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
|
||||
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"; }
|
||||
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.
|
||||
Device::Device(const std::filesystem::path &directory)
|
||||
: path_(directory / "boot-state.txt") {
|
||||
if (!std::filesystem::exists(path_)) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
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_);
|
||||
}
|
||||
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");
|
||||
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;
|
||||
}
|
||||
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
|
||||
}// namespace ota
|
||||
@@ -8,61 +8,69 @@
|
||||
|
||||
namespace ota {
|
||||
|
||||
enum class Phase {
|
||||
AConfirmed, PreparingB, BPrepared, TrialPending, TrialRunning,
|
||||
BConfirmed, RecoveryPending, RecoveryRunning, RecoveryChecked,
|
||||
SeparateRecoveryRequired
|
||||
};
|
||||
enum class Phase {
|
||||
AConfirmed,
|
||||
PreparingB,
|
||||
BPrepared,
|
||||
TrialPending,
|
||||
TrialRunning,
|
||||
BConfirmed,
|
||||
RecoveryPending,
|
||||
RecoveryRunning,
|
||||
RecoveryChecked,
|
||||
SeparateRecoveryRequired
|
||||
};
|
||||
|
||||
enum class Slot { A, B };
|
||||
enum class Slot { A,
|
||||
B };
|
||||
|
||||
const char* name(Phase phase);
|
||||
const char* name(Slot slot);
|
||||
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;
|
||||
};
|
||||
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);
|
||||
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_;
|
||||
};
|
||||
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_; }
|
||||
// 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_;
|
||||
};
|
||||
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);
|
||||
const std::vector<std::string> &scenarios();
|
||||
Device run_scenario(const std::string &scenario,
|
||||
const std::filesystem::path &directory,
|
||||
std::ostream *output = nullptr);
|
||||
|
||||
} // namespace ota
|
||||
}// namespace ota
|
||||
@@ -6,108 +6,107 @@
|
||||
#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;
|
||||
void expect(bool condition, const char *message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
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(); });
|
||||
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}) {
|
||||
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::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");
|
||||
expect(device.boot() == ota::Slot::A, "Exhausted trial did not return to A");
|
||||
expect(device.state().phase == ota::Phase::RecoveryRunning, "Recovery checks skipped");
|
||||
}
|
||||
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";
|
||||
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");
|
||||
}
|
||||
try {
|
||||
const ota::Device device(directory.path());
|
||||
} catch (const std::runtime_error&) {
|
||||
return;
|
||||
|
||||
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); });
|
||||
}
|
||||
throw std::runtime_error("Unreadable state was accepted");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
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 {
|
||||
@@ -117,7 +116,7 @@ int main() {
|
||||
confirmation_requires_boot();
|
||||
unreadable_state();
|
||||
std::cout << "All recovery invariant checks passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
} catch (const std::exception &error) {
|
||||
std::cerr << "Test failed: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user