Report unverified USB watchdog saves without promising rollback
This commit is contained in:
1 parent
5fc0943f59
commit
de0e18e8cc
3 files changed
+97
-34
No files matched your search
@@ -387,7 +387,7 @@ bool handleUsbLoggingWatchdogCommand(const char* command, char* reply,
|
||||
Config candidate = config;
|
||||
candidate.mode = mode;
|
||||
if (!saveConfig(candidate, true)) {
|
||||
snprintf(reply, capacity, "Error: USB watchdog state not durable; unchanged");
|
||||
snprintf(reply, capacity, "Error: save unconfirmed; reboot may differ");
|
||||
} else {
|
||||
config = candidate;
|
||||
if (config.last_event.sequence) config.last_event.persisted = true;
|
||||
|
||||
@@ -203,7 +203,7 @@ static void check_failclosed_load_and_exact_format() {
|
||||
assert(fixture.fs.files.at("/usb_wdg") == image(1));
|
||||
assert(mesh::usbLoggingStatus().persistence_ready);
|
||||
} else {
|
||||
assert(reply.find("not durable") != std::string::npos);
|
||||
assert(reply == "Error: save unconfirmed; reboot may differ");
|
||||
assert(!mesh::usbLoggingStatus().persistence_ready);
|
||||
}
|
||||
}
|
||||
@@ -236,8 +236,14 @@ static void check_backup_and_volatile_storage() {
|
||||
assert(mesh::usbLoggingStatus().watchdog_auto && !mesh::usbLoggingStatus().persistence_ready);
|
||||
assert(!fixture.run(0) && !fixture.run(Policy::MAX_MS));
|
||||
assert(recovery_calls.empty() && fixture.fs.write_opens == 0);
|
||||
assert(fixture.command("set usb.watchdog on").find("not durable") != std::string::npos);
|
||||
assert(fixture.fs.write_opens == 0); // Explicit repair cannot bless RAM storage.
|
||||
const auto before = fixture.fs.files;
|
||||
assert(fixture.command("set usb.watchdog on")
|
||||
== "Error: save unconfirmed; reboot may differ");
|
||||
assert(fixture.fs.files == before && fixture.fs.write_opens == 0
|
||||
&& fixture.fs.writes == 0 && fixture.fs.renames == 0 && fixture.fs.removes == 0);
|
||||
// Explicit repair cannot bless RAM storage or authorize automatic actions.
|
||||
assert(!fixture.run(Policy::MAX_MS + Policy::EARLY_RECOVERY_MS));
|
||||
assert(recovery_calls.empty() && probe_calls == 0 && fixture.fs.write_opens == 0);
|
||||
mesh::loadUsbLoggingWatchdog(nullptr);
|
||||
assert(!mesh::usbLoggingStatus().persistence_ready);
|
||||
assert(!mesh::isUsbLoggingWatchdogArmed());
|
||||
@@ -278,38 +284,94 @@ static void check_cli_is_strict_bounded_and_transactional() {
|
||||
}
|
||||
|
||||
static void check_failed_setter_and_readback_are_not_success() {
|
||||
for (unsigned fault = 0; fault != 5; ++fault) {
|
||||
Fixture fixture(0, 3, 7, 9);
|
||||
const auto old = fixture.fs.files.at("/usb_wdg");
|
||||
switch (fault) {
|
||||
case 0: fixture.fs.fail_write = true; break;
|
||||
case 1: fixture.fs.fail_open = "/usb_wdg.tmp"; break;
|
||||
case 2:
|
||||
fixture.fs.fail_rename_from = "/usb_wdg.tmp";
|
||||
fixture.fs.fail_rename_to = "/usb_wdg";
|
||||
break;
|
||||
case 3: fixture.fs.corrupt_final_image = true; break;
|
||||
case 4: fixture.fs.fail_final_readback = true; break;
|
||||
unsigned scenarios = 0;
|
||||
for (bool initially_on : {false, true}) {
|
||||
const uint8_t previous_mode = initially_on ? 1 : 0;
|
||||
const uint8_t candidate_mode = initially_on ? 0 : 1;
|
||||
const char* command = initially_on ? "set usb.watchdog off" : "set usb.watchdog on";
|
||||
const char* healthy_reply = initially_on ? "OK - USB watchdog off (saved)"
|
||||
: "OK - USB watchdog on (saved)";
|
||||
{ // Healthy setters retain their exact ACK and survive a fresh load.
|
||||
Fixture healthy(previous_mode, 3, 7, 9);
|
||||
assert(healthy.command(command) == healthy_reply);
|
||||
assert(healthy.fs.files.at("/usb_wdg") == image(candidate_mode, 3, 7, 9));
|
||||
assert(healthy.fs.write_opens == 1);
|
||||
healthy.reload();
|
||||
const auto status = mesh::usbLoggingStatus();
|
||||
assert(status.watchdog_enabled == !initially_on);
|
||||
assert(!status.watchdog_auto && status.persistence_ready);
|
||||
assert(status.backoff_step == 3 && status.recovery_count == 7 && status.reboot_count == 9);
|
||||
assert(healthy.fs.write_opens == 1); // Reload itself never writes.
|
||||
++scenarios;
|
||||
}
|
||||
for (unsigned fault = 0; fault != 5; ++fault) {
|
||||
Fixture fixture(previous_mode, 3, 7, 9);
|
||||
const auto old = fixture.fs.files.at("/usb_wdg");
|
||||
switch (fault) {
|
||||
case 0: fixture.fs.fail_write = true; break;
|
||||
case 1: fixture.fs.fail_open = "/usb_wdg.tmp"; break;
|
||||
case 2:
|
||||
fixture.fs.fail_rename_from = "/usb_wdg.tmp";
|
||||
fixture.fs.fail_rename_to = "/usb_wdg";
|
||||
break;
|
||||
case 3: fixture.fs.corrupt_final_image = true; break;
|
||||
case 4: fixture.fs.fail_final_readback = true; break;
|
||||
}
|
||||
const auto reply = fixture.command(command);
|
||||
assert(reply == "Error: save unconfirmed; reboot may differ");
|
||||
assert(reply.find("unchanged") == std::string::npos && reply.find("OK") == std::string::npos);
|
||||
const auto status = mesh::usbLoggingStatus();
|
||||
assert(status.watchdog_enabled == initially_on);
|
||||
assert(!status.watchdog_auto && !status.persistence_ready);
|
||||
assert(status.backoff_step == 3 && status.recovery_count == 7 && status.reboot_count == 9);
|
||||
if (fault <= 2) assert(fixture.fs.files.at("/usb_wdg") == old);
|
||||
if (fault == 4) {
|
||||
// Final readback cannot distinguish failed publication from a complete
|
||||
// candidate which is already durable. Do not claim disk rollback.
|
||||
assert(fixture.fs.files.at("/usb_wdg") == image(candidate_mode, 3, 7, 9));
|
||||
}
|
||||
const auto writes = fixture.fs.write_opens;
|
||||
assert(!fixture.run(0) && !fixture.run(Policy::MAX_MS));
|
||||
assert(recovery_calls.empty() && probe_calls == 0 && fixture.fs.write_opens == writes);
|
||||
fixture.fs.fail_write = false;
|
||||
fixture.fs.fail_open.clear();
|
||||
fixture.fs.fail_rename_from.clear();
|
||||
fixture.fs.fail_rename_to.clear();
|
||||
fixture.fs.corrupt_final_image = false;
|
||||
fixture.fs.fail_final_readback = false;
|
||||
// Merely clearing the fault cannot restore persistence authorization.
|
||||
assert(!fixture.run(Policy::MAX_MS + Policy::EARLY_RECOVERY_MS));
|
||||
assert(recovery_calls.empty() && probe_calls == 0 && fixture.fs.write_opens == writes);
|
||||
assert(!mesh::usbLoggingStatus().persistence_ready);
|
||||
assert(fixture.command(command) == healthy_reply);
|
||||
assert(fixture.fs.write_opens == writes + 1); // Explicit repair only.
|
||||
assert(fixture.fs.files.at("/usb_wdg") == image(candidate_mode, 3, 7, 9));
|
||||
assert(mesh::usbLoggingStatus().persistence_ready);
|
||||
assert(mesh::usbLoggingStatus().watchdog_enabled == !initially_on);
|
||||
++scenarios;
|
||||
}
|
||||
{ // Independently reboot before retry: the failed setter may be durable.
|
||||
Fixture fixture(previous_mode, 3, 7, 9);
|
||||
fixture.fs.fail_final_readback = true;
|
||||
assert(fixture.command(command) == "Error: save unconfirmed; reboot may differ");
|
||||
assert(mesh::usbLoggingStatus().watchdog_enabled == initially_on);
|
||||
assert(!mesh::usbLoggingStatus().persistence_ready);
|
||||
const auto writes = fixture.fs.write_opens;
|
||||
assert(!fixture.run(0) && !fixture.run(Policy::MAX_MS));
|
||||
assert(recovery_calls.empty() && probe_calls == 0 && fixture.fs.write_opens == writes);
|
||||
fixture.fs.fail_final_readback = false;
|
||||
fixture.reload();
|
||||
const auto reboot = mesh::usbLoggingStatus();
|
||||
assert(reboot.watchdog_enabled == !initially_on);
|
||||
assert(!reboot.watchdog_auto && reboot.persistence_ready);
|
||||
assert(reboot.backoff_step == 3 && reboot.recovery_count == 7 && reboot.reboot_count == 9);
|
||||
assert(fixture.fs.files.at("/usb_wdg") == image(candidate_mode, 3, 7, 9));
|
||||
assert(fixture.fs.write_opens == writes); // No retry was needed to publish.
|
||||
++scenarios;
|
||||
}
|
||||
assert(fixture.command("set usb.watchdog on").find("not durable; unchanged") != std::string::npos);
|
||||
const auto status = mesh::usbLoggingStatus();
|
||||
assert(!status.watchdog_enabled && !status.watchdog_auto && !status.persistence_ready);
|
||||
assert(status.backoff_step == 3 && status.recovery_count == 7 && status.reboot_count == 9);
|
||||
if (fault <= 2) assert(fixture.fs.files.at("/usb_wdg") == old);
|
||||
const auto writes = fixture.fs.write_opens;
|
||||
assert(!fixture.run(0) && !fixture.run(Policy::MAX_MS));
|
||||
assert(recovery_calls.empty() && fixture.fs.write_opens == writes);
|
||||
fixture.fs.fail_write = false;
|
||||
fixture.fs.fail_open.clear();
|
||||
fixture.fs.fail_rename_from.clear();
|
||||
fixture.fs.fail_rename_to.clear();
|
||||
fixture.fs.corrupt_final_image = false;
|
||||
fixture.fs.fail_final_readback = false;
|
||||
assert(fixture.command("set usb.watchdog on") == "OK - USB watchdog on (saved)");
|
||||
assert(fixture.fs.write_opens == writes + 1); // Manual retry, not automatic.
|
||||
assert(fixture.fs.files.at("/usb_wdg") == image(1, 3, 7, 9));
|
||||
assert(mesh::usbLoggingStatus().persistence_ready && mesh::usbLoggingStatus().watchdog_enabled);
|
||||
}
|
||||
assert(scenarios == 14);
|
||||
puts("14 USB watchdog setter durability scenarios passed");
|
||||
}
|
||||
|
||||
static void check_stages_durable_backoff_and_saturation() {
|
||||
|
||||
@@ -159,6 +159,7 @@ int main() {
|
||||
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(tested.returncode, 0, tested.stderr)
|
||||
self.assertIn('USB watchdog runtime and persistence passed', tested.stdout)
|
||||
self.assertIn('14 USB watchdog setter durability scenarios passed', tested.stdout)
|
||||
|
||||
def test_unsupported_transport_has_no_recovery_or_storage_work(self):
|
||||
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
|
||||
|
||||
Reference in new issue
Block a user