diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index 78f49b6..6346d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .venv build/ build_prev_runner/ -.vscode \ No newline at end of file diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24368c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "files.associations": { + "*.ino": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index a52ea23..45a96c1 100644 --- a/README.md +++ b/README.md @@ -101,5 +101,12 @@ If any guard rejects (or times out), the flow is: - `allow=false`: request is blocked immediately and reboot does not proceed. - `defer=true`: request is deferred immediately, remaining guards are skipped for the current pass, and evaluation restarts from the first guard after `deferTimeoutMs` (minimum 1ms when omitted/0). +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT - see [LICENSE.md](LICENSE.md). diff --git a/examples/basic_reboot_request/basic_reboot_request.ino b/examples/basic_reboot_request/basic_reboot_request.ino index 7c7b67c..24c823f 100644 --- a/examples/basic_reboot_request/basic_reboot_request.ino +++ b/examples/basic_reboot_request/basic_reboot_request.ino @@ -31,29 +31,29 @@ void setup() { rebootManager.onEvaluation([](const RebootEvaluation &evaluation) { if (evaluation.accepted) { Serial.printf( - "[reboot] accepted id=%lu reason=%s delay=%lu\n", - static_cast(evaluation.requestId), - evaluation.reason, - static_cast(evaluation.delayMs) - ); + "[reboot] accepted id=%lu reason=%s delay=%lu\n", + static_cast(evaluation.requestId), + evaluation.reason, + static_cast(evaluation.delayMs) + ); return; } Serial.printf( - "[reboot] rejected id=%lu code=%u blocker=%s detail=%s\n", - static_cast(evaluation.requestId), - static_cast(evaluation.code), - evaluation.blockerName, - evaluation.detail - ); + "[reboot] rejected id=%lu code=%u blocker=%s detail=%s\n", + static_cast(evaluation.requestId), + static_cast(evaluation.code), + evaluation.blockerName, + evaluation.detail + ); }); RebootSubmitResult result = rebootManager.requestReboot("ExampleReason", 1500); Serial.printf( - "submit status=%u requestId=%lu\n", - static_cast(result.status), - static_cast(result.requestId) - ); + "submit status=%u requestId=%lu\n", + static_cast(result.status), + static_cast(result.requestId) + ); } void loop() { diff --git a/examples/busy_and_retry/busy_and_retry.ino b/examples/busy_and_retry/busy_and_retry.ino index f555bc5..864c286 100644 --- a/examples/busy_and_retry/busy_and_retry.ino +++ b/examples/busy_and_retry/busy_and_retry.ino @@ -9,11 +9,11 @@ bool finished = false; void printSubmitResult(const char *label, const RebootSubmitResult &result) { Serial.printf( - "%s submit status=%u requestId=%lu\n", - label, - static_cast(result.status), - static_cast(result.requestId) - ); + "%s submit status=%u requestId=%lu\n", + label, + static_cast(result.status), + static_cast(result.requestId) + ); } void setup() { @@ -25,28 +25,24 @@ void setup() { ESPRebootManagerConfig config; config.taskName = "reboot-manager"; config.callbackTimeoutMs = 1000; - config.rebootExecutor = []() { - Serial.println("[executor] simulated reboot"); - }; + config.rebootExecutor = []() { Serial.println("[executor] simulated reboot"); }; if (!rebootManager.init(config)) { Serial.println("Failed to init ESPRebootManager"); return; } - rebootManager.onRebootRequest([](const RebootRequestContext &) { - return RebootVote{}; - }); + rebootManager.onRebootRequest([](const RebootRequestContext &) { return RebootVote{}; }); rebootManager.onEvaluation([](const RebootEvaluation &evaluation) { Serial.printf( - "[evaluation] accepted=%u code=%u id=%lu reason=%s delayMs=%lu\n", - static_cast(evaluation.accepted), - static_cast(evaluation.code), - static_cast(evaluation.requestId), - evaluation.reason, - static_cast(evaluation.delayMs) - ); + "[evaluation] accepted=%u code=%u id=%lu reason=%s delayMs=%lu\n", + static_cast(evaluation.accepted), + static_cast(evaluation.code), + static_cast(evaluation.requestId), + evaluation.reason, + static_cast(evaluation.delayMs) + ); }); RebootSubmitResult first = rebootManager.requestReboot("firmware-update", 4000); diff --git a/examples/callback_timeout/callback_timeout.ino b/examples/callback_timeout/callback_timeout.ino index 246f0eb..a18845b 100644 --- a/examples/callback_timeout/callback_timeout.ino +++ b/examples/callback_timeout/callback_timeout.ino @@ -35,12 +35,12 @@ void setup() { rebootManager.onEvaluation([](const RebootEvaluation &evaluation) { Serial.printf( - "[evaluation] accepted=%u code=%u blocker=%s detail=%s\n", - static_cast(evaluation.accepted), - static_cast(evaluation.code), - evaluation.blockerName, - evaluation.detail - ); + "[evaluation] accepted=%u code=%u blocker=%s detail=%s\n", + static_cast(evaluation.accepted), + static_cast(evaluation.code), + evaluation.blockerName, + evaluation.detail + ); }); RebootSubmitResult result = rebootManager.requestReboot("simulate-timeout", 0); @@ -57,11 +57,11 @@ void loop() { if (rebootManager.rebootStatus() == RebootRequestStatus::Idle) { const RebootEvaluation latest = rebootManager.lastEvaluation(); Serial.printf( - "final accepted=%u code=%u detail=%s\n", - static_cast(latest.accepted), - static_cast(latest.code), - latest.detail - ); + "final accepted=%u code=%u detail=%s\n", + static_cast(latest.accepted), + static_cast(latest.code), + latest.detail + ); printedFinal = true; } diff --git a/examples/guard_blocking/guard_blocking.ino b/examples/guard_blocking/guard_blocking.ino index 63af655..55fc2a2 100644 --- a/examples/guard_blocking/guard_blocking.ino +++ b/examples/guard_blocking/guard_blocking.ino @@ -17,9 +17,7 @@ void setup() { ESPRebootManagerConfig config; config.taskName = "reboot-manager"; config.callbackTimeoutMs = 1000; - config.rebootExecutor = []() { - Serial.println("[executor] simulated reboot"); - }; + config.rebootExecutor = []() { Serial.println("[executor] simulated reboot"); }; if (!rebootManager.init(config)) { Serial.println("Failed to init ESPRebootManager"); @@ -38,20 +36,20 @@ void setup() { rebootManager.onEvaluation([](const RebootEvaluation &evaluation) { if (evaluation.accepted) { Serial.printf( - "[evaluation] accepted id=%lu reason=%s\n", - static_cast(evaluation.requestId), - evaluation.reason - ); + "[evaluation] accepted id=%lu reason=%s\n", + static_cast(evaluation.requestId), + evaluation.reason + ); return; } Serial.printf( - "[evaluation] rejected id=%lu code=%u blocker=%s detail=%s\n", - static_cast(evaluation.requestId), - static_cast(evaluation.code), - evaluation.blockerName, - evaluation.detail - ); + "[evaluation] rejected id=%lu code=%u blocker=%s detail=%s\n", + static_cast(evaluation.requestId), + static_cast(evaluation.code), + evaluation.blockerName, + evaluation.detail + ); }); RebootSubmitResult first = rebootManager.requestReboot("scheduled-maintenance", 1000); @@ -67,7 +65,8 @@ void loop() { Serial.println("Maintenance window opened. Retrying reboot request..."); } - if (maintenanceWindowOpen && !secondQueued && rebootManager.rebootStatus() == RebootRequestStatus::Idle) { + if (maintenanceWindowOpen && !secondQueued && + rebootManager.rebootStatus() == RebootRequestStatus::Idle) { RebootSubmitResult second = rebootManager.requestReboot("scheduled-maintenance", 1000); secondQueued = (second.status == RebootSubmitStatus::Queued); Serial.printf("second submit status=%u\n", static_cast(second.status)); diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/esp_reboot_manager/reboot_manager.cpp b/src/esp_reboot_manager/reboot_manager.cpp index 71f74e5..37b2868 100644 --- a/src/esp_reboot_manager/reboot_manager.cpp +++ b/src/esp_reboot_manager/reboot_manager.cpp @@ -17,504 +17,521 @@ constexpr uint32_t kMinimumDeferTimeoutMs = 1; void defaultRebootExecutor() { #if __has_include() #if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) - ESP.restart(); + ESP.restart(); #endif #endif } uint32_t stackSizeWords(uint32_t stackSizeBytes) { - if( stackSizeBytes == 0 ){ - return 0; - } - const uint32_t wordSize = static_cast(sizeof(StackType_t)); - return (stackSizeBytes + wordSize - 1U) / wordSize; + if (stackSizeBytes == 0) { + return 0; + } + const uint32_t wordSize = static_cast(sizeof(StackType_t)); + return (stackSizeBytes + wordSize - 1U) / wordSize; } -} // namespace +} // namespace ESPRebootManager::~ESPRebootManager() { - deinit(); + deinit(); } -bool ESPRebootManager::init(const ESPRebootManagerConfig& config) { - if( initialized_.load(std::memory_order_acquire) ){ - deinit(); - } - - ESPRebootManagerConfig effective = config; - if( effective.taskName == nullptr || effective.taskName[0] == '\0' ){ - effective.taskName = "reboot-manager"; - } - if( effective.taskStackSizeBytes == 0 ){ - effective.taskStackSizeBytes = 6U * 1024U; - } - if( effective.callbackTimeoutMs == 0 ){ - effective.callbackTimeoutMs = 1000; - } - if( !effective.rebootExecutor ){ - effective.rebootExecutor = defaultRebootExecutor; - } - - signalSemaphore_ = xSemaphoreCreateBinaryStatic(&signalSemaphoreBuffer_); - if( signalSemaphore_ == nullptr ){ - return false; - } - - { - std::lock_guard lock(mutex_); - config_ = std::move(effective); - guards_.clear(); - evaluations_.clear(); - pendingRequest_ = PendingRequest{}; - lastEvaluation_ = RebootEvaluation{}; - } - - setStatus(RebootRequestStatus::Idle); - running_.store(true, std::memory_order_release); - - const uint32_t stackWords = stackSizeWords(config_.taskStackSizeBytes); - TaskHandle_t createdTaskHandle = nullptr; - BaseType_t created = xTaskCreatePinnedToCore( - &ESPRebootManager::taskEntry, - config_.taskName, - stackWords, - this, - config_.taskPriority, - &createdTaskHandle, - config_.taskCoreId - ); - - if( created != pdPASS ){ - running_.store(false, std::memory_order_release); - vSemaphoreDelete(signalSemaphore_); - signalSemaphore_ = nullptr; - taskHandle_.store(nullptr, std::memory_order_release); - return false; - } - - taskHandle_.store(createdTaskHandle, std::memory_order_release); - initialized_.store(true, std::memory_order_release); - return true; +bool ESPRebootManager::init(const ESPRebootManagerConfig &config) { + if (initialized_.load(std::memory_order_acquire)) { + deinit(); + } + + ESPRebootManagerConfig effective = config; + if (effective.taskName == nullptr || effective.taskName[0] == '\0') { + effective.taskName = "reboot-manager"; + } + if (effective.taskStackSizeBytes == 0) { + effective.taskStackSizeBytes = 6U * 1024U; + } + if (effective.callbackTimeoutMs == 0) { + effective.callbackTimeoutMs = 1000; + } + if (!effective.rebootExecutor) { + effective.rebootExecutor = defaultRebootExecutor; + } + + signalSemaphore_ = xSemaphoreCreateBinaryStatic(&signalSemaphoreBuffer_); + if (signalSemaphore_ == nullptr) { + return false; + } + + { + std::lock_guard lock(mutex_); + config_ = std::move(effective); + guards_.clear(); + evaluations_.clear(); + pendingRequest_ = PendingRequest{}; + lastEvaluation_ = RebootEvaluation{}; + } + + setStatus(RebootRequestStatus::Idle); + running_.store(true, std::memory_order_release); + + const uint32_t stackWords = stackSizeWords(config_.taskStackSizeBytes); + TaskHandle_t createdTaskHandle = nullptr; + BaseType_t created = xTaskCreatePinnedToCore( + &ESPRebootManager::taskEntry, + config_.taskName, + stackWords, + this, + config_.taskPriority, + &createdTaskHandle, + config_.taskCoreId + ); + + if (created != pdPASS) { + running_.store(false, std::memory_order_release); + vSemaphoreDelete(signalSemaphore_); + signalSemaphore_ = nullptr; + taskHandle_.store(nullptr, std::memory_order_release); + return false; + } + + taskHandle_.store(createdTaskHandle, std::memory_order_release); + initialized_.store(true, std::memory_order_release); + return true; } void ESPRebootManager::deinit() { - running_.store(false, std::memory_order_release); - - if( signalSemaphore_ != nullptr ){ - (void)xSemaphoreGive(signalSemaphore_); - } - - const uint32_t startMs = nowMs(); - while( - taskHandle_.load(std::memory_order_acquire) != nullptr && - (nowMs() - startMs) < kDeinitJoinTimeoutMs - ){ - vTaskDelay(pdMS_TO_TICKS(1)); - } - - TaskHandle_t taskHandle = taskHandle_.load(std::memory_order_acquire); - if( taskHandle != nullptr ){ - vTaskDelete(taskHandle); - taskHandle_.store(nullptr, std::memory_order_release); - } - - if( signalSemaphore_ != nullptr ){ - vSemaphoreDelete(signalSemaphore_); - signalSemaphore_ = nullptr; - } - - { - std::lock_guard lock(mutex_); - guards_.clear(); - evaluations_.clear(); - pendingRequest_ = PendingRequest{}; - lastEvaluation_ = RebootEvaluation{}; - config_ = ESPRebootManagerConfig{}; - } - - setStatus(RebootRequestStatus::Idle); - initialized_.store(false, std::memory_order_release); + running_.store(false, std::memory_order_release); + + if (signalSemaphore_ != nullptr) { + (void)xSemaphoreGive(signalSemaphore_); + } + + const uint32_t startMs = nowMs(); + while (taskHandle_.load(std::memory_order_acquire) != nullptr && + (nowMs() - startMs) < kDeinitJoinTimeoutMs) { + vTaskDelay(pdMS_TO_TICKS(1)); + } + + TaskHandle_t taskHandle = taskHandle_.load(std::memory_order_acquire); + if (taskHandle != nullptr) { + vTaskDelete(taskHandle); + taskHandle_.store(nullptr, std::memory_order_release); + } + + if (signalSemaphore_ != nullptr) { + vSemaphoreDelete(signalSemaphore_); + signalSemaphore_ = nullptr; + } + + { + std::lock_guard lock(mutex_); + guards_.clear(); + evaluations_.clear(); + pendingRequest_ = PendingRequest{}; + lastEvaluation_ = RebootEvaluation{}; + config_ = ESPRebootManagerConfig{}; + } + + setStatus(RebootRequestStatus::Idle); + initialized_.store(false, std::memory_order_release); } bool ESPRebootManager::isInitialized() const { - return initialized_.load(std::memory_order_acquire); + return initialized_.load(std::memory_order_acquire); } RebootCallbackId ESPRebootManager::onRebootRequest(GuardCallback cb) { - if( !cb ){ - return 0; - } - - const RebootCallbackId id = nextCallbackId_.fetch_add(1, std::memory_order_acq_rel); - - std::lock_guard lock(mutex_); - GuardEntry entry{}; - entry.id = id; - entry.callback = std::move(cb); - entry.active = true; - guards_.push_back(std::move(entry)); - return id; + if (!cb) { + return 0; + } + + const RebootCallbackId id = nextCallbackId_.fetch_add(1, std::memory_order_acq_rel); + + std::lock_guard lock(mutex_); + GuardEntry entry{}; + entry.id = id; + entry.callback = std::move(cb); + entry.active = true; + guards_.push_back(std::move(entry)); + return id; } RebootCallbackId ESPRebootManager::onEvaluation(EvaluationCallback cb) { - if( !cb ){ - return 0; - } - - const RebootCallbackId id = nextCallbackId_.fetch_add(1, std::memory_order_acq_rel); - - std::lock_guard lock(mutex_); - EvaluationEntry entry{}; - entry.id = id; - entry.callback = std::move(cb); - entry.active = true; - evaluations_.push_back(std::move(entry)); - return id; + if (!cb) { + return 0; + } + + const RebootCallbackId id = nextCallbackId_.fetch_add(1, std::memory_order_acq_rel); + + std::lock_guard lock(mutex_); + EvaluationEntry entry{}; + entry.id = id; + entry.callback = std::move(cb); + entry.active = true; + evaluations_.push_back(std::move(entry)); + return id; } bool ESPRebootManager::offRebootRequest(RebootCallbackId id) { - if( id == 0 ){ - return false; - } - - std::lock_guard lock(mutex_); - bool removed = false; - for( GuardEntry& entry : guards_ ){ - if( entry.id == id && entry.active ){ - entry.active = false; - removed = true; - break; - } - } - if( removed ){ - compactGuards(guards_); - } - return removed; + if (id == 0) { + return false; + } + + std::lock_guard lock(mutex_); + bool removed = false; + for (GuardEntry &entry : guards_) { + if (entry.id == id && entry.active) { + entry.active = false; + removed = true; + break; + } + } + if (removed) { + compactGuards(guards_); + } + return removed; } bool ESPRebootManager::offEvaluation(RebootCallbackId id) { - if( id == 0 ){ - return false; - } - - std::lock_guard lock(mutex_); - bool removed = false; - for( EvaluationEntry& entry : evaluations_ ){ - if( entry.id == id && entry.active ){ - entry.active = false; - removed = true; - break; - } - } - if( removed ){ - compactEvaluations(evaluations_); - } - return removed; + if (id == 0) { + return false; + } + + std::lock_guard lock(mutex_); + bool removed = false; + for (EvaluationEntry &entry : evaluations_) { + if (entry.id == id && entry.active) { + entry.active = false; + removed = true; + break; + } + } + if (removed) { + compactEvaluations(evaluations_); + } + return removed; } -RebootSubmitResult ESPRebootManager::requestReboot(const char* reason, uint32_t delayMs) { - RebootSubmitResult result{}; - - if( !isInitialized() ){ - result.status = RebootSubmitStatus::NotInitialized; - return result; - } - - if( reason == nullptr || reason[0] == '\0' ){ - result.status = RebootSubmitStatus::InvalidArgument; - return result; - } - - if( std::strlen(reason) >= sizeof(RebootRequestContext::reason) ){ - result.status = RebootSubmitStatus::InvalidArgument; - return result; - } - - RebootRequestContext context{}; - context.requestId = nextRequestId_.fetch_add(1, std::memory_order_acq_rel); - copyText(reason, context.reason, sizeof(context.reason)); - context.delayMs = delayMs; - context.requestedAtMs = nowMs(); - - { - std::lock_guard lock(mutex_); - if( status_.load(std::memory_order_acquire) != RebootRequestStatus::Idle || pendingRequest_.pending ){ - result.status = RebootSubmitStatus::Busy; - return result; - } - - pendingRequest_.pending = true; - pendingRequest_.request = context; - setStatus(RebootRequestStatus::Requested); - } - - if( signalSemaphore_ == nullptr || xSemaphoreGive(signalSemaphore_) != pdTRUE ){ - std::lock_guard lock(mutex_); - pendingRequest_.pending = false; - setStatus(RebootRequestStatus::Idle); - result.status = RebootSubmitStatus::InternalError; - return result; - } - - result.status = RebootSubmitStatus::Queued; - result.requestId = context.requestId; - return result; +RebootSubmitResult ESPRebootManager::requestReboot(const char *reason, uint32_t delayMs) { + RebootSubmitResult result{}; + + if (!isInitialized()) { + result.status = RebootSubmitStatus::NotInitialized; + return result; + } + + if (reason == nullptr || reason[0] == '\0') { + result.status = RebootSubmitStatus::InvalidArgument; + return result; + } + + if (std::strlen(reason) >= sizeof(RebootRequestContext::reason)) { + result.status = RebootSubmitStatus::InvalidArgument; + return result; + } + + RebootRequestContext context{}; + context.requestId = nextRequestId_.fetch_add(1, std::memory_order_acq_rel); + copyText(reason, context.reason, sizeof(context.reason)); + context.delayMs = delayMs; + context.requestedAtMs = nowMs(); + + { + std::lock_guard lock(mutex_); + if (status_.load(std::memory_order_acquire) != RebootRequestStatus::Idle || + pendingRequest_.pending) { + result.status = RebootSubmitStatus::Busy; + return result; + } + + pendingRequest_.pending = true; + pendingRequest_.request = context; + setStatus(RebootRequestStatus::Requested); + } + + if (signalSemaphore_ == nullptr || xSemaphoreGive(signalSemaphore_) != pdTRUE) { + std::lock_guard lock(mutex_); + pendingRequest_.pending = false; + setStatus(RebootRequestStatus::Idle); + result.status = RebootSubmitStatus::InternalError; + return result; + } + + result.status = RebootSubmitStatus::Queued; + result.requestId = context.requestId; + return result; } bool ESPRebootManager::isRebootRequested() const { - return rebootStatus() != RebootRequestStatus::Idle; + return rebootStatus() != RebootRequestStatus::Idle; } RebootRequestStatus ESPRebootManager::rebootStatus() const { - return status_.load(std::memory_order_acquire); + return status_.load(std::memory_order_acquire); } RebootEvaluation ESPRebootManager::lastEvaluation() const { - std::lock_guard lock(mutex_); - return lastEvaluation_; + std::lock_guard lock(mutex_); + return lastEvaluation_; } -void ESPRebootManager::taskEntry(void* arg) { - ESPRebootManager* manager = static_cast(arg); - if( manager != nullptr ){ - manager->taskLoop(); - } +void ESPRebootManager::taskEntry(void *arg) { + ESPRebootManager *manager = static_cast(arg); + if (manager != nullptr) { + manager->taskLoop(); + } } void ESPRebootManager::taskLoop() { - while( running_.load(std::memory_order_acquire) ){ - if( signalSemaphore_ == nullptr ){ - break; - } - - if( xSemaphoreTake(signalSemaphore_, portMAX_DELAY) != pdTRUE ){ - continue; - } - - if( !running_.load(std::memory_order_acquire) ){ - break; - } - - RebootRequestContext request{}; - bool hasRequest = false; - - { - std::lock_guard lock(mutex_); - if( pendingRequest_.pending ){ - request = pendingRequest_.request; - pendingRequest_.pending = false; - hasRequest = true; - } - } - - if( !hasRequest ){ - continue; - } - - processRequest(request); - } - - taskHandle_.store(nullptr, std::memory_order_release); + while (running_.load(std::memory_order_acquire)) { + if (signalSemaphore_ == nullptr) { + break; + } + + if (xSemaphoreTake(signalSemaphore_, portMAX_DELAY) != pdTRUE) { + continue; + } + + if (!running_.load(std::memory_order_acquire)) { + break; + } + + RebootRequestContext request{}; + bool hasRequest = false; + + { + std::lock_guard lock(mutex_); + if (pendingRequest_.pending) { + request = pendingRequest_.request; + pendingRequest_.pending = false; + hasRequest = true; + } + } + + if (!hasRequest) { + continue; + } + + processRequest(request); + } + + taskHandle_.store(nullptr, std::memory_order_release); } -void ESPRebootManager::processRequest(const RebootRequestContext& request) { - while( running_.load(std::memory_order_acquire) ){ - setStatus(RebootRequestStatus::Evaluating); - - RebootEvaluation evaluation{}; - evaluation.requestId = request.requestId; - copyText(request.reason, evaluation.reason, sizeof(evaluation.reason)); - evaluation.delayMs = request.delayMs; - - const std::vector guards = guardSnapshot(); - bool deferred = false; - - for( const GuardEntry& guard : guards ){ - if( !guard.callback ){ - continue; - } - - const uint32_t startedMs = nowMs(); - RebootVote vote = guard.callback(request); - const uint32_t elapsedMs = nowMs() - startedMs; - - if( config_.callbackTimeoutMs > 0 && elapsedMs > config_.callbackTimeoutMs ){ - evaluation.accepted = false; - evaluation.code = RebootDecisionCode::CallbackTimeout; - formatBlockerName(guard.id, evaluation.blockerName, sizeof(evaluation.blockerName)); - if( vote.detail[0] != '\0' ){ - copyText(vote.detail, evaluation.detail, sizeof(evaluation.detail)); - } else { - copyText("guard callback exceeded timeout", evaluation.detail, sizeof(evaluation.detail)); - } - evaluation.evaluatedAtMs = nowMs(); - emitEvaluation(evaluation); - setStatus(RebootRequestStatus::Idle); - return; - } - - if( vote.defer ){ - const uint32_t deferTimeoutMs = - vote.deferTimeoutMs > 0 ? vote.deferTimeoutMs : kMinimumDeferTimeoutMs; - evaluation.accepted = false; - evaluation.code = RebootDecisionCode::Deferred; - evaluation.deferTimeoutMs = deferTimeoutMs; - formatBlockerName(guard.id, evaluation.blockerName, sizeof(evaluation.blockerName)); - if( vote.detail[0] != '\0' ){ - copyText(vote.detail, evaluation.detail, sizeof(evaluation.detail)); - } else { - copyText("deferred by guard callback", evaluation.detail, sizeof(evaluation.detail)); - } - evaluation.evaluatedAtMs = nowMs(); - emitEvaluation(evaluation); - - setStatus(RebootRequestStatus::Deferred); - vTaskDelay(pdMS_TO_TICKS(deferTimeoutMs)); - if( !running_.load(std::memory_order_acquire) ){ - setStatus(RebootRequestStatus::Idle); - return; - } - - deferred = true; - break; - } - - if( !vote.allow ){ - evaluation.accepted = false; - evaluation.code = RebootDecisionCode::Blocked; - formatBlockerName(guard.id, evaluation.blockerName, sizeof(evaluation.blockerName)); - if( vote.detail[0] != '\0' ){ - copyText(vote.detail, evaluation.detail, sizeof(evaluation.detail)); - } else { - copyText("blocked by guard callback", evaluation.detail, sizeof(evaluation.detail)); - } - evaluation.evaluatedAtMs = nowMs(); - emitEvaluation(evaluation); - setStatus(RebootRequestStatus::Idle); - return; - } - } - - if( deferred ){ - continue; - } - - evaluation.accepted = true; - evaluation.code = RebootDecisionCode::Accepted; - evaluation.evaluatedAtMs = nowMs(); - emitEvaluation(evaluation); - - setStatus(RebootRequestStatus::Delaying); - if( request.delayMs > 0 ){ - vTaskDelay(pdMS_TO_TICKS(request.delayMs)); - } - - if( !running_.load(std::memory_order_acquire) ){ - setStatus(RebootRequestStatus::Idle); - return; - } - - setStatus(RebootRequestStatus::Rebooting); - std::function rebootExecutor; - - { - std::lock_guard lock(mutex_); - rebootExecutor = config_.rebootExecutor; - } - - if( rebootExecutor ){ - rebootExecutor(); - } - - setStatus(RebootRequestStatus::Idle); - return; - } - - setStatus(RebootRequestStatus::Idle); +void ESPRebootManager::processRequest(const RebootRequestContext &request) { + while (running_.load(std::memory_order_acquire)) { + setStatus(RebootRequestStatus::Evaluating); + + RebootEvaluation evaluation{}; + evaluation.requestId = request.requestId; + copyText(request.reason, evaluation.reason, sizeof(evaluation.reason)); + evaluation.delayMs = request.delayMs; + + const std::vector guards = guardSnapshot(); + bool deferred = false; + + for (const GuardEntry &guard : guards) { + if (!guard.callback) { + continue; + } + + const uint32_t startedMs = nowMs(); + RebootVote vote = guard.callback(request); + const uint32_t elapsedMs = nowMs() - startedMs; + + if (config_.callbackTimeoutMs > 0 && elapsedMs > config_.callbackTimeoutMs) { + evaluation.accepted = false; + evaluation.code = RebootDecisionCode::CallbackTimeout; + formatBlockerName(guard.id, evaluation.blockerName, sizeof(evaluation.blockerName)); + if (vote.detail[0] != '\0') { + copyText(vote.detail, evaluation.detail, sizeof(evaluation.detail)); + } else { + copyText( + "guard callback exceeded timeout", + evaluation.detail, + sizeof(evaluation.detail) + ); + } + evaluation.evaluatedAtMs = nowMs(); + emitEvaluation(evaluation); + setStatus(RebootRequestStatus::Idle); + return; + } + + if (vote.defer) { + const uint32_t deferTimeoutMs = + vote.deferTimeoutMs > 0 ? vote.deferTimeoutMs : kMinimumDeferTimeoutMs; + evaluation.accepted = false; + evaluation.code = RebootDecisionCode::Deferred; + evaluation.deferTimeoutMs = deferTimeoutMs; + formatBlockerName(guard.id, evaluation.blockerName, sizeof(evaluation.blockerName)); + if (vote.detail[0] != '\0') { + copyText(vote.detail, evaluation.detail, sizeof(evaluation.detail)); + } else { + copyText( + "deferred by guard callback", + evaluation.detail, + sizeof(evaluation.detail) + ); + } + evaluation.evaluatedAtMs = nowMs(); + emitEvaluation(evaluation); + + setStatus(RebootRequestStatus::Deferred); + vTaskDelay(pdMS_TO_TICKS(deferTimeoutMs)); + if (!running_.load(std::memory_order_acquire)) { + setStatus(RebootRequestStatus::Idle); + return; + } + + deferred = true; + break; + } + + if (!vote.allow) { + evaluation.accepted = false; + evaluation.code = RebootDecisionCode::Blocked; + formatBlockerName(guard.id, evaluation.blockerName, sizeof(evaluation.blockerName)); + if (vote.detail[0] != '\0') { + copyText(vote.detail, evaluation.detail, sizeof(evaluation.detail)); + } else { + copyText( + "blocked by guard callback", + evaluation.detail, + sizeof(evaluation.detail) + ); + } + evaluation.evaluatedAtMs = nowMs(); + emitEvaluation(evaluation); + setStatus(RebootRequestStatus::Idle); + return; + } + } + + if (deferred) { + continue; + } + + evaluation.accepted = true; + evaluation.code = RebootDecisionCode::Accepted; + evaluation.evaluatedAtMs = nowMs(); + emitEvaluation(evaluation); + + setStatus(RebootRequestStatus::Delaying); + if (request.delayMs > 0) { + vTaskDelay(pdMS_TO_TICKS(request.delayMs)); + } + + if (!running_.load(std::memory_order_acquire)) { + setStatus(RebootRequestStatus::Idle); + return; + } + + setStatus(RebootRequestStatus::Rebooting); + std::function rebootExecutor; + + { + std::lock_guard lock(mutex_); + rebootExecutor = config_.rebootExecutor; + } + + if (rebootExecutor) { + rebootExecutor(); + } + + setStatus(RebootRequestStatus::Idle); + return; + } + + setStatus(RebootRequestStatus::Idle); } -void ESPRebootManager::emitEvaluation(const RebootEvaluation& evaluation) { - std::vector callbacks; +void ESPRebootManager::emitEvaluation(const RebootEvaluation &evaluation) { + std::vector callbacks; - { - std::lock_guard lock(mutex_); - lastEvaluation_ = evaluation; - callbacks = evaluationSnapshot(); - } + { + std::lock_guard lock(mutex_); + lastEvaluation_ = evaluation; + callbacks = evaluationSnapshot(); + } - for( const EvaluationEntry& callback : callbacks ){ - if( callback.callback ){ - callback.callback(evaluation); - } - } + for (const EvaluationEntry &callback : callbacks) { + if (callback.callback) { + callback.callback(evaluation); + } + } } -void ESPRebootManager::copyText(const char* source, char* destination, size_t destinationSize) { - if( destination == nullptr || destinationSize == 0 ){ - return; - } +void ESPRebootManager::copyText(const char *source, char *destination, size_t destinationSize) { + if (destination == nullptr || destinationSize == 0) { + return; + } - if( source == nullptr ){ - destination[0] = '\0'; - return; - } + if (source == nullptr) { + destination[0] = '\0'; + return; + } - std::snprintf(destination, destinationSize, "%s", source); + std::snprintf(destination, destinationSize, "%s", source); } -void ESPRebootManager::compactGuards(std::vector& entries) { - entries.erase( - std::remove_if(entries.begin(), entries.end(), [](const GuardEntry& entry) { return !entry.active; }), - entries.end() - ); +void ESPRebootManager::compactGuards(std::vector &entries) { + entries.erase( + std::remove_if( + entries.begin(), + entries.end(), + [](const GuardEntry &entry) { return !entry.active; } + ), + entries.end() + ); } -void ESPRebootManager::compactEvaluations(std::vector& entries) { - entries.erase( - std::remove_if(entries.begin(), entries.end(), [](const EvaluationEntry& entry) { return !entry.active; }), - entries.end() - ); +void ESPRebootManager::compactEvaluations(std::vector &entries) { + entries.erase( + std::remove_if( + entries.begin(), + entries.end(), + [](const EvaluationEntry &entry) { return !entry.active; } + ), + entries.end() + ); } void ESPRebootManager::formatBlockerName( - RebootCallbackId id, - char* destination, - size_t destinationSize + RebootCallbackId id, char *destination, size_t destinationSize ) { - if( destination == nullptr || destinationSize == 0 ){ - return; - } + if (destination == nullptr || destinationSize == 0) { + return; + } - std::snprintf(destination, destinationSize, "guard:%lu", static_cast(id)); + std::snprintf(destination, destinationSize, "guard:%lu", static_cast(id)); } std::vector ESPRebootManager::guardSnapshot() { - std::lock_guard lock(mutex_); - std::vector snapshot; - snapshot.reserve(guards_.size()); - for( const GuardEntry& guard : guards_ ){ - if( guard.active && guard.callback ){ - snapshot.push_back(guard); - } - } - return snapshot; + std::lock_guard lock(mutex_); + std::vector snapshot; + snapshot.reserve(guards_.size()); + for (const GuardEntry &guard : guards_) { + if (guard.active && guard.callback) { + snapshot.push_back(guard); + } + } + return snapshot; } std::vector ESPRebootManager::evaluationSnapshot() { - std::vector snapshot; - snapshot.reserve(evaluations_.size()); - for( const EvaluationEntry& entry : evaluations_ ){ - if( entry.active && entry.callback ){ - snapshot.push_back(entry); - } - } - return snapshot; + std::vector snapshot; + snapshot.reserve(evaluations_.size()); + for (const EvaluationEntry &entry : evaluations_) { + if (entry.active && entry.callback) { + snapshot.push_back(entry); + } + } + return snapshot; } uint32_t ESPRebootManager::nowMs() const { - return static_cast(xTaskGetTickCount() * portTICK_PERIOD_MS); + return static_cast(xTaskGetTickCount() * portTICK_PERIOD_MS); } void ESPRebootManager::setStatus(RebootRequestStatus status) { - status_.store(status, std::memory_order_release); + status_.store(status, std::memory_order_release); } diff --git a/src/esp_reboot_manager/reboot_manager.h b/src/esp_reboot_manager/reboot_manager.h index 9d81b3c..acd5da6 100644 --- a/src/esp_reboot_manager/reboot_manager.h +++ b/src/esp_reboot_manager/reboot_manager.h @@ -16,143 +16,143 @@ extern "C" { using RebootCallbackId = uint32_t; enum class RebootRequestStatus : uint8_t { - Idle = 0, - Requested = 1, - Evaluating = 2, - Deferred = 3, - Delaying = 4, - Rebooting = 5, + Idle = 0, + Requested = 1, + Evaluating = 2, + Deferred = 3, + Delaying = 4, + Rebooting = 5, }; enum class RebootSubmitStatus : uint8_t { - Queued = 0, - Busy = 1, - InvalidArgument = 2, - NotInitialized = 3, - InternalError = 4, + Queued = 0, + Busy = 1, + InvalidArgument = 2, + NotInitialized = 3, + InternalError = 4, }; enum class RebootDecisionCode : uint8_t { - Accepted = 0, - Blocked = 1, - CallbackTimeout = 2, - InvalidArgument = 3, - InternalError = 4, - Deferred = 5, + Accepted = 0, + Blocked = 1, + CallbackTimeout = 2, + InvalidArgument = 3, + InternalError = 4, + Deferred = 5, }; struct RebootRequestContext { - uint32_t requestId = 0; - char reason[64] = {}; - uint32_t delayMs = 0; - uint32_t requestedAtMs = 0; + uint32_t requestId = 0; + char reason[64] = {}; + uint32_t delayMs = 0; + uint32_t requestedAtMs = 0; }; struct RebootVote { - bool allow = true; - bool defer = false; - uint32_t deferTimeoutMs = 0; - char detail[96] = {}; + bool allow = true; + bool defer = false; + uint32_t deferTimeoutMs = 0; + char detail[96] = {}; }; struct RebootEvaluation { - uint32_t requestId = 0; - bool accepted = false; - RebootDecisionCode code = RebootDecisionCode::InternalError; - char reason[64] = {}; - uint32_t delayMs = 0; - uint32_t deferTimeoutMs = 0; - char blockerName[32] = {}; - char detail[96] = {}; - uint32_t evaluatedAtMs = 0; + uint32_t requestId = 0; + bool accepted = false; + RebootDecisionCode code = RebootDecisionCode::InternalError; + char reason[64] = {}; + uint32_t delayMs = 0; + uint32_t deferTimeoutMs = 0; + char blockerName[32] = {}; + char detail[96] = {}; + uint32_t evaluatedAtMs = 0; }; struct RebootSubmitResult { - RebootSubmitStatus status = RebootSubmitStatus::InternalError; - uint32_t requestId = 0; + RebootSubmitStatus status = RebootSubmitStatus::InternalError; + uint32_t requestId = 0; }; struct ESPRebootManagerConfig { - const char* taskName = "reboot-manager"; - uint32_t taskStackSizeBytes = 6 * 1024; - UBaseType_t taskPriority = 1; - BaseType_t taskCoreId = tskNO_AFFINITY; - uint32_t callbackTimeoutMs = 1000; - std::function rebootExecutor; + const char *taskName = "reboot-manager"; + uint32_t taskStackSizeBytes = 6 * 1024; + UBaseType_t taskPriority = 1; + BaseType_t taskCoreId = tskNO_AFFINITY; + uint32_t callbackTimeoutMs = 1000; + std::function rebootExecutor; }; class ESPRebootManager { - public: - using GuardCallback = std::function; - using EvaluationCallback = std::function; - - ESPRebootManager() = default; - ~ESPRebootManager(); - - bool init(const ESPRebootManagerConfig& config = {}); - void deinit(); - bool isInitialized() const; - - RebootCallbackId onRebootRequest(GuardCallback cb); - RebootCallbackId onEvaluation(EvaluationCallback cb); - bool offRebootRequest(RebootCallbackId id); - bool offEvaluation(RebootCallbackId id); - - RebootSubmitResult requestReboot(const char* reason, uint32_t delayMs = 0); - - bool isRebootRequested() const; - RebootRequestStatus rebootStatus() const; - RebootEvaluation lastEvaluation() const; - - private: - struct GuardEntry { - RebootCallbackId id = 0; - GuardCallback callback; - bool active = false; - }; - - struct EvaluationEntry { - RebootCallbackId id = 0; - EvaluationCallback callback; - bool active = false; - }; - - struct PendingRequest { - bool pending = false; - RebootRequestContext request; - }; - - static void taskEntry(void* arg); - void taskLoop(); - void processRequest(const RebootRequestContext& request); - void emitEvaluation(const RebootEvaluation& evaluation); - - static void copyText(const char* source, char* destination, size_t destinationSize); - static void compactGuards(std::vector& entries); - static void compactEvaluations(std::vector& entries); - static void formatBlockerName(RebootCallbackId id, char* destination, size_t destinationSize); - - std::vector guardSnapshot(); - std::vector evaluationSnapshot(); - - uint32_t nowMs() const; - void setStatus(RebootRequestStatus status); - - ESPRebootManagerConfig config_{}; - - std::atomic initialized_{false}; - std::atomic running_{false}; - std::atomic status_{RebootRequestStatus::Idle}; - std::atomic nextCallbackId_{1}; - std::atomic nextRequestId_{1}; - - std::atomic taskHandle_{nullptr}; - SemaphoreHandle_t signalSemaphore_{nullptr}; - StaticSemaphore_t signalSemaphoreBuffer_{}; - - mutable std::mutex mutex_; - std::vector guards_{}; - std::vector evaluations_{}; - PendingRequest pendingRequest_{}; - RebootEvaluation lastEvaluation_{}; + public: + using GuardCallback = std::function; + using EvaluationCallback = std::function; + + ESPRebootManager() = default; + ~ESPRebootManager(); + + bool init(const ESPRebootManagerConfig &config = {}); + void deinit(); + bool isInitialized() const; + + RebootCallbackId onRebootRequest(GuardCallback cb); + RebootCallbackId onEvaluation(EvaluationCallback cb); + bool offRebootRequest(RebootCallbackId id); + bool offEvaluation(RebootCallbackId id); + + RebootSubmitResult requestReboot(const char *reason, uint32_t delayMs = 0); + + bool isRebootRequested() const; + RebootRequestStatus rebootStatus() const; + RebootEvaluation lastEvaluation() const; + + private: + struct GuardEntry { + RebootCallbackId id = 0; + GuardCallback callback; + bool active = false; + }; + + struct EvaluationEntry { + RebootCallbackId id = 0; + EvaluationCallback callback; + bool active = false; + }; + + struct PendingRequest { + bool pending = false; + RebootRequestContext request; + }; + + static void taskEntry(void *arg); + void taskLoop(); + void processRequest(const RebootRequestContext &request); + void emitEvaluation(const RebootEvaluation &evaluation); + + static void copyText(const char *source, char *destination, size_t destinationSize); + static void compactGuards(std::vector &entries); + static void compactEvaluations(std::vector &entries); + static void formatBlockerName(RebootCallbackId id, char *destination, size_t destinationSize); + + std::vector guardSnapshot(); + std::vector evaluationSnapshot(); + + uint32_t nowMs() const; + void setStatus(RebootRequestStatus status); + + ESPRebootManagerConfig config_{}; + + std::atomic initialized_{false}; + std::atomic running_{false}; + std::atomic status_{RebootRequestStatus::Idle}; + std::atomic nextCallbackId_{1}; + std::atomic nextRequestId_{1}; + + std::atomic taskHandle_{nullptr}; + SemaphoreHandle_t signalSemaphore_{nullptr}; + StaticSemaphore_t signalSemaphoreBuffer_{}; + + mutable std::mutex mutex_; + std::vector guards_{}; + std::vector evaluations_{}; + PendingRequest pendingRequest_{}; + RebootEvaluation lastEvaluation_{}; }; diff --git a/test/reboot_manager_test_stubs.cpp b/test/reboot_manager_test_stubs.cpp index ef95534..24ac661 100644 --- a/test/reboot_manager_test_stubs.cpp +++ b/test/reboot_manager_test_stubs.cpp @@ -13,16 +13,16 @@ namespace { struct FakeSemaphore { - std::mutex mutex; - std::condition_variable condition; - bool available = false; - bool deleted = false; + std::mutex mutex; + std::condition_variable condition; + bool available = false; + bool deleted = false; }; struct FakeTask { - TaskFunction_t entry = nullptr; - void* arg = nullptr; - std::thread worker; + TaskFunction_t entry = nullptr; + void *arg = nullptr; + std::thread worker; }; std::atomic g_createdTasks{0}; @@ -31,177 +31,175 @@ std::atomic g_deletedTasks{0}; const auto g_startTime = std::chrono::steady_clock::now(); thread_local TaskHandle_t g_currentTaskHandle = nullptr; -} // namespace +} // namespace extern "C" unsigned long millis(void) { - const auto elapsed = std::chrono::steady_clock::now() - g_startTime; - return static_cast( - std::chrono::duration_cast(elapsed).count() - ); + const auto elapsed = std::chrono::steady_clock::now() - g_startTime; + return static_cast( + std::chrono::duration_cast(elapsed).count() + ); } -extern "C" SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t* /*buffer*/) { - FakeSemaphore* semaphore = new (std::nothrow) FakeSemaphore{}; - return reinterpret_cast(semaphore); +extern "C" SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t * /*buffer*/) { + FakeSemaphore *semaphore = new (std::nothrow) FakeSemaphore{}; + return reinterpret_cast(semaphore); } extern "C" BaseType_t xSemaphoreTake(SemaphoreHandle_t handle, TickType_t ticks) { - if( handle == nullptr ){ - return pdFALSE; - } - - FakeSemaphore* semaphore = reinterpret_cast(handle); - std::unique_lock lock(semaphore->mutex); - - auto ready = [semaphore]() { - return semaphore->available || semaphore->deleted; - }; - - if( ticks == 0 ){ - if( !ready() ){ - return pdFALSE; - } - } else if( ticks == portMAX_DELAY ){ - semaphore->condition.wait(lock, ready); - } else { - const auto timeout = std::chrono::milliseconds(static_cast(ticks)); - if( !semaphore->condition.wait_for(lock, timeout, ready) ){ - return pdFALSE; - } - } - - if( semaphore->deleted ){ - return pdFALSE; - } - - if( !semaphore->available ){ - return pdFALSE; - } - - semaphore->available = false; - return pdTRUE; + if (handle == nullptr) { + return pdFALSE; + } + + FakeSemaphore *semaphore = reinterpret_cast(handle); + std::unique_lock lock(semaphore->mutex); + + auto ready = [semaphore]() { return semaphore->available || semaphore->deleted; }; + + if (ticks == 0) { + if (!ready()) { + return pdFALSE; + } + } else if (ticks == portMAX_DELAY) { + semaphore->condition.wait(lock, ready); + } else { + const auto timeout = std::chrono::milliseconds(static_cast(ticks)); + if (!semaphore->condition.wait_for(lock, timeout, ready)) { + return pdFALSE; + } + } + + if (semaphore->deleted) { + return pdFALSE; + } + + if (!semaphore->available) { + return pdFALSE; + } + + semaphore->available = false; + return pdTRUE; } extern "C" BaseType_t xSemaphoreGive(SemaphoreHandle_t handle) { - if( handle == nullptr ){ - return pdFALSE; - } - - FakeSemaphore* semaphore = reinterpret_cast(handle); - { - std::lock_guard lock(semaphore->mutex); - if( semaphore->deleted ){ - return pdFALSE; - } - semaphore->available = true; - } - semaphore->condition.notify_one(); - return pdTRUE; + if (handle == nullptr) { + return pdFALSE; + } + + FakeSemaphore *semaphore = reinterpret_cast(handle); + { + std::lock_guard lock(semaphore->mutex); + if (semaphore->deleted) { + return pdFALSE; + } + semaphore->available = true; + } + semaphore->condition.notify_one(); + return pdTRUE; } extern "C" void vSemaphoreDelete(SemaphoreHandle_t handle) { - if( handle == nullptr ){ - return; - } - - FakeSemaphore* semaphore = reinterpret_cast(handle); - { - std::lock_guard lock(semaphore->mutex); - semaphore->deleted = true; - semaphore->available = false; - } - semaphore->condition.notify_all(); - delete semaphore; + if (handle == nullptr) { + return; + } + + FakeSemaphore *semaphore = reinterpret_cast(handle); + { + std::lock_guard lock(semaphore->mutex); + semaphore->deleted = true; + semaphore->available = false; + } + semaphore->condition.notify_all(); + delete semaphore; } extern "C" BaseType_t xTaskCreatePinnedToCore( TaskFunction_t task, - const char* /*name*/, + const char * /*name*/, uint32_t /*stackDepth*/, - void* parameters, + void *parameters, UBaseType_t /*priority*/, - TaskHandle_t* createdTask, + TaskHandle_t *createdTask, BaseType_t /*coreId*/ ) { - if( task == nullptr ){ - return pdFAIL; - } - - FakeTask* fakeTask = new (std::nothrow) FakeTask{}; - if( fakeTask == nullptr ){ - return pdFAIL; - } - - fakeTask->entry = task; - fakeTask->arg = parameters; - - TaskHandle_t handle = reinterpret_cast(fakeTask); - if( createdTask != nullptr ){ - *createdTask = handle; - } - - fakeTask->worker = std::thread([fakeTask, handle]() { - g_currentTaskHandle = handle; - fakeTask->entry(fakeTask->arg); - g_currentTaskHandle = nullptr; - }); - - g_createdTasks.fetch_add(1, std::memory_order_relaxed); - return pdPASS; + if (task == nullptr) { + return pdFAIL; + } + + FakeTask *fakeTask = new (std::nothrow) FakeTask{}; + if (fakeTask == nullptr) { + return pdFAIL; + } + + fakeTask->entry = task; + fakeTask->arg = parameters; + + TaskHandle_t handle = reinterpret_cast(fakeTask); + if (createdTask != nullptr) { + *createdTask = handle; + } + + fakeTask->worker = std::thread([fakeTask, handle]() { + g_currentTaskHandle = handle; + fakeTask->entry(fakeTask->arg); + g_currentTaskHandle = nullptr; + }); + + g_createdTasks.fetch_add(1, std::memory_order_relaxed); + return pdPASS; } extern "C" void vTaskDelete(TaskHandle_t task) { - TaskHandle_t target = task; - if( target == nullptr ){ - target = g_currentTaskHandle; - } - - if( target == nullptr ){ - return; - } - - FakeTask* fakeTask = reinterpret_cast(target); - - if( fakeTask->worker.joinable() ){ - if( std::this_thread::get_id() != fakeTask->worker.get_id() ){ - fakeTask->worker.join(); - } else { - fakeTask->worker.detach(); - } - } - - delete fakeTask; - g_deletedTasks.fetch_add(1, std::memory_order_relaxed); + TaskHandle_t target = task; + if (target == nullptr) { + target = g_currentTaskHandle; + } + + if (target == nullptr) { + return; + } + + FakeTask *fakeTask = reinterpret_cast(target); + + if (fakeTask->worker.joinable()) { + if (std::this_thread::get_id() != fakeTask->worker.get_id()) { + fakeTask->worker.join(); + } else { + fakeTask->worker.detach(); + } + } + + delete fakeTask; + g_deletedTasks.fetch_add(1, std::memory_order_relaxed); } extern "C" void vTaskDelay(TickType_t ticks) { - std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(ticks))); + std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(ticks))); } extern "C" TickType_t xTaskGetTickCount(void) { - const auto elapsed = std::chrono::steady_clock::now() - g_startTime; - return static_cast( - std::chrono::duration_cast(elapsed).count() - ); + const auto elapsed = std::chrono::steady_clock::now() - g_startTime; + return static_cast( + std::chrono::duration_cast(elapsed).count() + ); } extern "C" TaskHandle_t xTaskGetCurrentTaskHandle(void) { - return g_currentTaskHandle; + return g_currentTaskHandle; } namespace test_support { void resetRuntime() { - g_createdTasks.store(0, std::memory_order_relaxed); - g_deletedTasks.store(0, std::memory_order_relaxed); + g_createdTasks.store(0, std::memory_order_relaxed); + g_deletedTasks.store(0, std::memory_order_relaxed); } size_t createdTaskCount() { - return g_createdTasks.load(std::memory_order_relaxed); + return g_createdTasks.load(std::memory_order_relaxed); } size_t deletedTaskCount() { - return g_deletedTasks.load(std::memory_order_relaxed); + return g_deletedTasks.load(std::memory_order_relaxed); } -} // namespace test_support +} // namespace test_support diff --git a/test/stubs/freertos/FreeRTOS.h b/test/stubs/freertos/FreeRTOS.h index f6433e2..03f7ed7 100644 --- a/test/stubs/freertos/FreeRTOS.h +++ b/test/stubs/freertos/FreeRTOS.h @@ -11,12 +11,12 @@ typedef int BaseType_t; typedef unsigned int UBaseType_t; typedef uint32_t TickType_t; typedef uint32_t configSTACK_DEPTH_TYPE; -typedef void* TaskHandle_t; -typedef void* SemaphoreHandle_t; +typedef void *TaskHandle_t; +typedef void *SemaphoreHandle_t; typedef uint32_t StackType_t; typedef struct StaticSemaphore { - uintptr_t storage[8]; + uintptr_t storage[8]; } StaticSemaphore_t; #define pdTRUE 1 @@ -24,7 +24,7 @@ typedef struct StaticSemaphore { #define pdPASS 1 #define pdFAIL 0 -#define portMAX_DELAY ((TickType_t)-1) +#define portMAX_DELAY ((TickType_t) - 1) #define portTICK_PERIOD_MS 1 #define tskNO_AFFINITY (-1) diff --git a/test/stubs/freertos/semphr.h b/test/stubs/freertos/semphr.h index 71388ce..db1f638 100644 --- a/test/stubs/freertos/semphr.h +++ b/test/stubs/freertos/semphr.h @@ -6,7 +6,7 @@ extern "C" { #endif -SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t* buffer); +SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *buffer); BaseType_t xSemaphoreTake(SemaphoreHandle_t handle, TickType_t ticks); BaseType_t xSemaphoreGive(SemaphoreHandle_t handle); void vSemaphoreDelete(SemaphoreHandle_t handle); diff --git a/test/stubs/freertos/task.h b/test/stubs/freertos/task.h index 601c888..e7f7ef4 100644 --- a/test/stubs/freertos/task.h +++ b/test/stubs/freertos/task.h @@ -6,15 +6,17 @@ extern "C" { #endif -typedef void (*TaskFunction_t)(void*); +typedef void (*TaskFunction_t)(void *); -BaseType_t xTaskCreatePinnedToCore(TaskFunction_t task, - const char* name, - uint32_t stackDepth, - void* parameters, - UBaseType_t priority, - TaskHandle_t* createdTask, - BaseType_t coreId); +BaseType_t xTaskCreatePinnedToCore( + TaskFunction_t task, + const char *name, + uint32_t stackDepth, + void *parameters, + UBaseType_t priority, + TaskHandle_t *createdTask, + BaseType_t coreId +); void vTaskDelete(TaskHandle_t task); void vTaskDelay(TickType_t ticks); diff --git a/test/test_esp_reboot_manager/test_esp_reboot_manager.cpp b/test/test_esp_reboot_manager/test_esp_reboot_manager.cpp index 36a8574..66e2ddb 100644 --- a/test/test_esp_reboot_manager/test_esp_reboot_manager.cpp +++ b/test/test_esp_reboot_manager/test_esp_reboot_manager.cpp @@ -15,400 +15,537 @@ namespace { -[[noreturn]] void fail(const std::string& message) { - throw std::runtime_error(message); +[[noreturn]] void fail(const std::string &message) { + throw std::runtime_error(message); } -void expectTrue(bool condition, const std::string& message) { - if( !condition ){ - fail(message); - } +void expectTrue(bool condition, const std::string &message) { + if (!condition) { + fail(message); + } } -void expectFalse(bool condition, const std::string& message) { - if( condition ){ - fail(message); - } +void expectFalse(bool condition, const std::string &message) { + if (condition) { + fail(message); + } } template -void expectEqual(const T& actual, const T& expected, const std::string& message) { - if( !(actual == expected) ){ - fail(message); - } +void expectEqual(const T &actual, const T &expected, const std::string &message) { + if (!(actual == expected)) { + fail(message); + } } -bool waitUntil(const std::function& predicate, uint32_t timeoutMs) { - const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); - while( std::chrono::steady_clock::now() < deadline ){ - if( predicate() ){ - return true; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - return predicate(); +bool waitUntil(const std::function &predicate, uint32_t timeoutMs) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + while (std::chrono::steady_clock::now() < deadline) { + if (predicate()) { + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return predicate(); } void testInitDeinitLifecycle() { - test_support::resetRuntime(); + test_support::resetRuntime(); - ESPRebootManager manager; - expectFalse(manager.isInitialized(), "manager should start uninitialized"); + ESPRebootManager manager; + expectFalse(manager.isInitialized(), "manager should start uninitialized"); - manager.deinit(); - expectFalse(manager.isInitialized(), "deinit before init should be safe"); + manager.deinit(); + expectFalse(manager.isInitialized(), "deinit before init should be safe"); - expectTrue(manager.init(), "init should succeed"); - expectTrue(manager.isInitialized(), "manager should be initialized"); + expectTrue(manager.init(), "init should succeed"); + expectTrue(manager.isInitialized(), "manager should be initialized"); - manager.deinit(); - expectFalse(manager.isInitialized(), "manager should deinitialize cleanly"); + manager.deinit(); + expectFalse(manager.isInitialized(), "manager should deinitialize cleanly"); - manager.deinit(); - expectFalse(manager.isInitialized(), "deinit should be idempotent"); + manager.deinit(); + expectFalse(manager.isInitialized(), "deinit should be idempotent"); - expectTrue(test_support::createdTaskCount() >= static_cast(1), "init should create worker task"); + expectTrue( + test_support::createdTaskCount() >= static_cast(1), + "init should create worker task" + ); } void testCallbackRegistrationAndUnregister() { - ESPRebootManager manager; - expectTrue(manager.init(), "init should succeed"); + ESPRebootManager manager; + expectTrue(manager.init(), "init should succeed"); - RebootCallbackId guardId = manager.onRebootRequest([](const RebootRequestContext&) { - return RebootVote{}; - }); - RebootCallbackId evalId = manager.onEvaluation([](const RebootEvaluation&) {}); + RebootCallbackId guardId = + manager.onRebootRequest([](const RebootRequestContext &) { return RebootVote{}; }); + RebootCallbackId evalId = manager.onEvaluation([](const RebootEvaluation &) {}); - expectTrue(guardId > 0, "guard callback registration should return valid id"); - expectTrue(evalId > 0, "evaluation callback registration should return valid id"); + expectTrue(guardId > 0, "guard callback registration should return valid id"); + expectTrue(evalId > 0, "evaluation callback registration should return valid id"); - expectTrue(manager.offRebootRequest(guardId), "guard callback should be removable"); - expectFalse(manager.offRebootRequest(guardId), "removed guard callback should not be removable twice"); + expectTrue(manager.offRebootRequest(guardId), "guard callback should be removable"); + expectFalse( + manager.offRebootRequest(guardId), + "removed guard callback should not be removable twice" + ); - expectTrue(manager.offEvaluation(evalId), "evaluation callback should be removable"); - expectFalse(manager.offEvaluation(evalId), "removed evaluation callback should not be removable twice"); + expectTrue(manager.offEvaluation(evalId), "evaluation callback should be removable"); + expectFalse( + manager.offEvaluation(evalId), + "removed evaluation callback should not be removable twice" + ); - manager.deinit(); + manager.deinit(); } void testRequestValidationAndBusyHandling() { - ESPRebootManager manager; - ESPRebootManagerConfig cfg{}; - cfg.rebootExecutor = []() {}; - - expectTrue(manager.init(cfg), "init should succeed"); - - RebootSubmitResult nullReason = manager.requestReboot(nullptr, 0); - expectEqual(nullReason.status, RebootSubmitStatus::InvalidArgument, "null reason should be invalid"); - - RebootSubmitResult emptyReason = manager.requestReboot("", 0); - expectEqual(emptyReason.status, RebootSubmitStatus::InvalidArgument, "empty reason should be invalid"); - - const char* tooLongReason = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890++++"; - RebootSubmitResult longReason = manager.requestReboot(tooLongReason, 0); - expectEqual(longReason.status, RebootSubmitStatus::InvalidArgument, "long reason should be invalid"); - - manager.onRebootRequest([](const RebootRequestContext&) { - vTaskDelay(pdMS_TO_TICKS(30)); - return RebootVote{}; - }); - - RebootSubmitResult first = manager.requestReboot("normal", 60); - expectEqual(first.status, RebootSubmitStatus::Queued, "first request should queue"); - - RebootSubmitResult second = manager.requestReboot("parallel", 0); - expectEqual(second.status, RebootSubmitStatus::Busy, "second request should return busy while first is active"); - - expectTrue(waitUntil([&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, 500), - "request should eventually return to idle"); - - manager.deinit(); + ESPRebootManager manager; + ESPRebootManagerConfig cfg{}; + cfg.rebootExecutor = []() {}; + + expectTrue(manager.init(cfg), "init should succeed"); + + RebootSubmitResult nullReason = manager.requestReboot(nullptr, 0); + expectEqual( + nullReason.status, + RebootSubmitStatus::InvalidArgument, + "null reason should be invalid" + ); + + RebootSubmitResult emptyReason = manager.requestReboot("", 0); + expectEqual( + emptyReason.status, + RebootSubmitStatus::InvalidArgument, + "empty reason should be invalid" + ); + + const char *tooLongReason = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890++++"; + RebootSubmitResult longReason = manager.requestReboot(tooLongReason, 0); + expectEqual( + longReason.status, + RebootSubmitStatus::InvalidArgument, + "long reason should be invalid" + ); + + manager.onRebootRequest([](const RebootRequestContext &) { + vTaskDelay(pdMS_TO_TICKS(30)); + return RebootVote{}; + }); + + RebootSubmitResult first = manager.requestReboot("normal", 60); + expectEqual(first.status, RebootSubmitStatus::Queued, "first request should queue"); + + RebootSubmitResult second = manager.requestReboot("parallel", 0); + expectEqual( + second.status, + RebootSubmitStatus::Busy, + "second request should return busy while first is active" + ); + + expectTrue( + waitUntil( + [&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, + 500 + ), + "request should eventually return to idle" + ); + + manager.deinit(); } void testAcceptedFlowAndRebootExecution() { - ESPRebootManager manager; - - std::atomic evaluationCount{0}; - std::atomic rebootCount{0}; - RebootEvaluation captured{}; - - ESPRebootManagerConfig cfg{}; - cfg.rebootExecutor = [&rebootCount]() { - rebootCount.fetch_add(1, std::memory_order_relaxed); - }; - - expectTrue(manager.init(cfg), "init should succeed"); - - manager.onRebootRequest([](const RebootRequestContext&) { - vTaskDelay(pdMS_TO_TICKS(5)); - return RebootVote{}; - }); - manager.onRebootRequest([](const RebootRequestContext&) { - return RebootVote{}; - }); - - manager.onEvaluation([&evaluationCount, &captured](const RebootEvaluation& evaluation) { - captured = evaluation; - evaluationCount.fetch_add(1, std::memory_order_relaxed); - }); - - RebootSubmitResult result = manager.requestReboot("accepted", 20); - expectEqual(result.status, RebootSubmitStatus::Queued, "accepted test request should queue"); - - expectTrue(waitUntil([&evaluationCount]() { return evaluationCount.load(std::memory_order_relaxed) == 1; }, 500), - "evaluation callback should fire once for accepted request"); - - expectTrue(captured.accepted, "accepted flow should report accepted evaluation"); - expectEqual(captured.code, RebootDecisionCode::Accepted, "accepted flow should use Accepted code"); - - expectTrue(waitUntil([&rebootCount]() { return rebootCount.load(std::memory_order_relaxed) == 1; }, 500), - "accepted flow should invoke reboot executor once"); - - expectTrue(waitUntil([&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, 500), - "accepted flow should return to idle after executor returns"); - expectFalse(manager.isRebootRequested(), "isRebootRequested should be false in idle state"); - - manager.deinit(); + ESPRebootManager manager; + + std::atomic evaluationCount{0}; + std::atomic rebootCount{0}; + RebootEvaluation captured{}; + + ESPRebootManagerConfig cfg{}; + cfg.rebootExecutor = [&rebootCount]() { rebootCount.fetch_add(1, std::memory_order_relaxed); }; + + expectTrue(manager.init(cfg), "init should succeed"); + + manager.onRebootRequest([](const RebootRequestContext &) { + vTaskDelay(pdMS_TO_TICKS(5)); + return RebootVote{}; + }); + manager.onRebootRequest([](const RebootRequestContext &) { return RebootVote{}; }); + + manager.onEvaluation([&evaluationCount, &captured](const RebootEvaluation &evaluation) { + captured = evaluation; + evaluationCount.fetch_add(1, std::memory_order_relaxed); + }); + + RebootSubmitResult result = manager.requestReboot("accepted", 20); + expectEqual(result.status, RebootSubmitStatus::Queued, "accepted test request should queue"); + + expectTrue( + waitUntil( + [&evaluationCount]() { return evaluationCount.load(std::memory_order_relaxed) == 1; }, + 500 + ), + "evaluation callback should fire once for accepted request" + ); + + expectTrue(captured.accepted, "accepted flow should report accepted evaluation"); + expectEqual( + captured.code, + RebootDecisionCode::Accepted, + "accepted flow should use Accepted code" + ); + + expectTrue( + waitUntil( + [&rebootCount]() { return rebootCount.load(std::memory_order_relaxed) == 1; }, + 500 + ), + "accepted flow should invoke reboot executor once" + ); + + expectTrue( + waitUntil( + [&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, + 500 + ), + "accepted flow should return to idle after executor returns" + ); + expectFalse(manager.isRebootRequested(), "isRebootRequested should be false in idle state"); + + manager.deinit(); } void testBlockedFlow() { - ESPRebootManager manager; - - std::atomic rebootCount{0}; - RebootEvaluation captured{}; - - ESPRebootManagerConfig cfg{}; - cfg.rebootExecutor = [&rebootCount]() { - rebootCount.fetch_add(1, std::memory_order_relaxed); - }; - - expectTrue(manager.init(cfg), "init should succeed"); - - manager.onRebootRequest([](const RebootRequestContext&) { - return RebootVote{}; - }); - - manager.onRebootRequest([](const RebootRequestContext&) { - RebootVote vote{}; - vote.allow = false; - std::snprintf(vote.detail, sizeof(vote.detail), "module2 not ready"); - return vote; - }); - - std::atomic evaluationCount{0}; - manager.onEvaluation([&captured, &evaluationCount](const RebootEvaluation& evaluation) { - captured = evaluation; - evaluationCount.fetch_add(1, std::memory_order_relaxed); - }); - - RebootSubmitResult result = manager.requestReboot("blocked", 20); - expectEqual(result.status, RebootSubmitStatus::Queued, "blocked test request should queue"); - - expectTrue(waitUntil([&evaluationCount]() { return evaluationCount.load(std::memory_order_relaxed) == 1; }, 500), - "blocked request should emit exactly one evaluation"); - - expectFalse(captured.accepted, "blocked flow should report rejected evaluation"); - expectEqual(captured.code, RebootDecisionCode::Blocked, "blocked flow should report blocked decision code"); - expectTrue(std::strstr(captured.detail, "module2") != nullptr, "blocked detail should include blocker reason"); - - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - expectEqual(rebootCount.load(std::memory_order_relaxed), 0, "blocked flow must not invoke reboot executor"); - - expectTrue(waitUntil([&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, 200), - "blocked flow should return to idle"); - - manager.deinit(); + ESPRebootManager manager; + + std::atomic rebootCount{0}; + RebootEvaluation captured{}; + + ESPRebootManagerConfig cfg{}; + cfg.rebootExecutor = [&rebootCount]() { rebootCount.fetch_add(1, std::memory_order_relaxed); }; + + expectTrue(manager.init(cfg), "init should succeed"); + + manager.onRebootRequest([](const RebootRequestContext &) { return RebootVote{}; }); + + manager.onRebootRequest([](const RebootRequestContext &) { + RebootVote vote{}; + vote.allow = false; + std::snprintf(vote.detail, sizeof(vote.detail), "module2 not ready"); + return vote; + }); + + std::atomic evaluationCount{0}; + manager.onEvaluation([&captured, &evaluationCount](const RebootEvaluation &evaluation) { + captured = evaluation; + evaluationCount.fetch_add(1, std::memory_order_relaxed); + }); + + RebootSubmitResult result = manager.requestReboot("blocked", 20); + expectEqual(result.status, RebootSubmitStatus::Queued, "blocked test request should queue"); + + expectTrue( + waitUntil( + [&evaluationCount]() { return evaluationCount.load(std::memory_order_relaxed) == 1; }, + 500 + ), + "blocked request should emit exactly one evaluation" + ); + + expectFalse(captured.accepted, "blocked flow should report rejected evaluation"); + expectEqual( + captured.code, + RebootDecisionCode::Blocked, + "blocked flow should report blocked decision code" + ); + expectTrue( + std::strstr(captured.detail, "module2") != nullptr, + "blocked detail should include blocker reason" + ); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + expectEqual( + rebootCount.load(std::memory_order_relaxed), + 0, + "blocked flow must not invoke reboot executor" + ); + + expectTrue( + waitUntil( + [&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, + 200 + ), + "blocked flow should return to idle" + ); + + manager.deinit(); } void testCallbackTimeoutFlow() { - ESPRebootManager manager; - - std::atomic rebootCount{0}; - RebootEvaluation captured{}; - - ESPRebootManagerConfig cfg{}; - cfg.callbackTimeoutMs = 10; - cfg.rebootExecutor = [&rebootCount]() { - rebootCount.fetch_add(1, std::memory_order_relaxed); - }; - - expectTrue(manager.init(cfg), "init should succeed"); - - manager.onRebootRequest([](const RebootRequestContext&) { - vTaskDelay(pdMS_TO_TICKS(25)); - return RebootVote{}; - }); - - std::atomic evaluationCount{0}; - manager.onEvaluation([&captured, &evaluationCount](const RebootEvaluation& evaluation) { - captured = evaluation; - evaluationCount.fetch_add(1, std::memory_order_relaxed); - }); - - RebootSubmitResult result = manager.requestReboot("timeout", 0); - expectEqual(result.status, RebootSubmitStatus::Queued, "timeout test request should queue"); - - expectTrue(waitUntil([&evaluationCount]() { return evaluationCount.load(std::memory_order_relaxed) == 1; }, 500), - "timeout request should emit one evaluation"); - - expectFalse(captured.accepted, "timeout flow should reject the reboot"); - expectEqual(captured.code, RebootDecisionCode::CallbackTimeout, "timeout flow should report callback timeout"); - expectEqual(rebootCount.load(std::memory_order_relaxed), 0, "timeout flow must not reboot"); - - manager.deinit(); + ESPRebootManager manager; + + std::atomic rebootCount{0}; + RebootEvaluation captured{}; + + ESPRebootManagerConfig cfg{}; + cfg.callbackTimeoutMs = 10; + cfg.rebootExecutor = [&rebootCount]() { rebootCount.fetch_add(1, std::memory_order_relaxed); }; + + expectTrue(manager.init(cfg), "init should succeed"); + + manager.onRebootRequest([](const RebootRequestContext &) { + vTaskDelay(pdMS_TO_TICKS(25)); + return RebootVote{}; + }); + + std::atomic evaluationCount{0}; + manager.onEvaluation([&captured, &evaluationCount](const RebootEvaluation &evaluation) { + captured = evaluation; + evaluationCount.fetch_add(1, std::memory_order_relaxed); + }); + + RebootSubmitResult result = manager.requestReboot("timeout", 0); + expectEqual(result.status, RebootSubmitStatus::Queued, "timeout test request should queue"); + + expectTrue( + waitUntil( + [&evaluationCount]() { return evaluationCount.load(std::memory_order_relaxed) == 1; }, + 500 + ), + "timeout request should emit one evaluation" + ); + + expectFalse(captured.accepted, "timeout flow should reject the reboot"); + expectEqual( + captured.code, + RebootDecisionCode::CallbackTimeout, + "timeout flow should report callback timeout" + ); + expectEqual(rebootCount.load(std::memory_order_relaxed), 0, "timeout flow must not reboot"); + + manager.deinit(); } void testDeferredFlowRetriesFromFirstGuard() { - ESPRebootManager manager; - - std::atomic rebootCount{0}; - std::atomic firstGuardCalls{0}; - std::atomic secondGuardCalls{0}; - std::atomic thirdGuardCalls{0}; - std::atomic deferredEvaluations{0}; - std::atomic acceptedEvaluations{0}; - std::atomic capturedDeferTimeoutMs{0}; - - ESPRebootManagerConfig cfg{}; - cfg.rebootExecutor = [&rebootCount]() { - rebootCount.fetch_add(1, std::memory_order_relaxed); - }; - - expectTrue(manager.init(cfg), "init should succeed"); - - manager.onRebootRequest([&firstGuardCalls](const RebootRequestContext&) { - firstGuardCalls.fetch_add(1, std::memory_order_relaxed); - return RebootVote{}; - }); - - manager.onRebootRequest([&secondGuardCalls](const RebootRequestContext&) { - const int callNumber = secondGuardCalls.fetch_add(1, std::memory_order_relaxed) + 1; - RebootVote vote{}; - if( callNumber == 1 ){ - vote.defer = true; - vote.deferTimeoutMs = 60; - std::snprintf(vote.detail, sizeof(vote.detail), "waiting for storage flush"); - } - return vote; - }); - - manager.onRebootRequest([&thirdGuardCalls](const RebootRequestContext&) { - thirdGuardCalls.fetch_add(1, std::memory_order_relaxed); - return RebootVote{}; - }); - - manager.onEvaluation([&deferredEvaluations, &acceptedEvaluations, &capturedDeferTimeoutMs](const RebootEvaluation& evaluation) { - if( evaluation.code == RebootDecisionCode::Deferred ){ - deferredEvaluations.fetch_add(1, std::memory_order_relaxed); - capturedDeferTimeoutMs.store(evaluation.deferTimeoutMs, std::memory_order_relaxed); - } - if( evaluation.code == RebootDecisionCode::Accepted ){ - acceptedEvaluations.fetch_add(1, std::memory_order_relaxed); - } - }); - - RebootSubmitResult result = manager.requestReboot("deferred-flow", 0); - expectEqual(result.status, RebootSubmitStatus::Queued, "deferred test request should queue"); - - expectTrue(waitUntil([&manager]() { return manager.rebootStatus() == RebootRequestStatus::Deferred; }, 300), - "deferred flow should transition to Deferred status"); - - expectTrue(waitUntil([&rebootCount]() { return rebootCount.load(std::memory_order_relaxed) == 1; }, 1000), - "deferred flow should eventually reboot once guards allow"); - - expectEqual(firstGuardCalls.load(std::memory_order_relaxed), 2, "first guard should run once per pass"); - expectEqual(secondGuardCalls.load(std::memory_order_relaxed), 2, "second guard should run once per pass"); - expectEqual(thirdGuardCalls.load(std::memory_order_relaxed), 1, "third guard should be skipped on deferred pass"); - expectEqual(deferredEvaluations.load(std::memory_order_relaxed), 1, "deferred flow should emit one deferred evaluation"); - expectEqual(acceptedEvaluations.load(std::memory_order_relaxed), 1, "deferred flow should emit one accepted evaluation"); - expectEqual(capturedDeferTimeoutMs.load(std::memory_order_relaxed), 60U, "evaluation should expose defer timeout"); - - expectTrue(waitUntil([&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, 300), - "deferred flow should return to idle"); - - manager.deinit(); + ESPRebootManager manager; + + std::atomic rebootCount{0}; + std::atomic firstGuardCalls{0}; + std::atomic secondGuardCalls{0}; + std::atomic thirdGuardCalls{0}; + std::atomic deferredEvaluations{0}; + std::atomic acceptedEvaluations{0}; + std::atomic capturedDeferTimeoutMs{0}; + + ESPRebootManagerConfig cfg{}; + cfg.rebootExecutor = [&rebootCount]() { rebootCount.fetch_add(1, std::memory_order_relaxed); }; + + expectTrue(manager.init(cfg), "init should succeed"); + + manager.onRebootRequest([&firstGuardCalls](const RebootRequestContext &) { + firstGuardCalls.fetch_add(1, std::memory_order_relaxed); + return RebootVote{}; + }); + + manager.onRebootRequest([&secondGuardCalls](const RebootRequestContext &) { + const int callNumber = secondGuardCalls.fetch_add(1, std::memory_order_relaxed) + 1; + RebootVote vote{}; + if (callNumber == 1) { + vote.defer = true; + vote.deferTimeoutMs = 60; + std::snprintf(vote.detail, sizeof(vote.detail), "waiting for storage flush"); + } + return vote; + }); + + manager.onRebootRequest([&thirdGuardCalls](const RebootRequestContext &) { + thirdGuardCalls.fetch_add(1, std::memory_order_relaxed); + return RebootVote{}; + }); + + manager.onEvaluation([&deferredEvaluations, + &acceptedEvaluations, + &capturedDeferTimeoutMs](const RebootEvaluation &evaluation) { + if (evaluation.code == RebootDecisionCode::Deferred) { + deferredEvaluations.fetch_add(1, std::memory_order_relaxed); + capturedDeferTimeoutMs.store(evaluation.deferTimeoutMs, std::memory_order_relaxed); + } + if (evaluation.code == RebootDecisionCode::Accepted) { + acceptedEvaluations.fetch_add(1, std::memory_order_relaxed); + } + }); + + RebootSubmitResult result = manager.requestReboot("deferred-flow", 0); + expectEqual(result.status, RebootSubmitStatus::Queued, "deferred test request should queue"); + + expectTrue( + waitUntil( + [&manager]() { return manager.rebootStatus() == RebootRequestStatus::Deferred; }, + 300 + ), + "deferred flow should transition to Deferred status" + ); + + expectTrue( + waitUntil( + [&rebootCount]() { return rebootCount.load(std::memory_order_relaxed) == 1; }, + 1000 + ), + "deferred flow should eventually reboot once guards allow" + ); + + expectEqual( + firstGuardCalls.load(std::memory_order_relaxed), + 2, + "first guard should run once per pass" + ); + expectEqual( + secondGuardCalls.load(std::memory_order_relaxed), + 2, + "second guard should run once per pass" + ); + expectEqual( + thirdGuardCalls.load(std::memory_order_relaxed), + 1, + "third guard should be skipped on deferred pass" + ); + expectEqual( + deferredEvaluations.load(std::memory_order_relaxed), + 1, + "deferred flow should emit one deferred evaluation" + ); + expectEqual( + acceptedEvaluations.load(std::memory_order_relaxed), + 1, + "deferred flow should emit one accepted evaluation" + ); + expectEqual( + capturedDeferTimeoutMs.load(std::memory_order_relaxed), + 60U, + "evaluation should expose defer timeout" + ); + + expectTrue( + waitUntil( + [&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, + 300 + ), + "deferred flow should return to idle" + ); + + manager.deinit(); } void testDeferredStateRemainsBusyForNewRequests() { - ESPRebootManager manager; - - std::atomic guardCalls{0}; - - ESPRebootManagerConfig cfg{}; - cfg.rebootExecutor = []() {}; - expectTrue(manager.init(cfg), "init should succeed"); - - manager.onRebootRequest([&guardCalls](const RebootRequestContext&) { - const int callNumber = guardCalls.fetch_add(1, std::memory_order_relaxed) + 1; - RebootVote vote{}; - if( callNumber == 1 ){ - vote.defer = true; - vote.deferTimeoutMs = 80; - std::snprintf(vote.detail, sizeof(vote.detail), "finish write queue"); - } - return vote; - }); - - RebootSubmitResult first = manager.requestReboot("primary", 0); - expectEqual(first.status, RebootSubmitStatus::Queued, "first deferred request should queue"); - - expectTrue(waitUntil([&manager]() { return manager.rebootStatus() == RebootRequestStatus::Deferred; }, 300), - "manager should expose Deferred status during retry wait"); - - RebootSubmitResult second = manager.requestReboot("secondary", 0); - expectEqual(second.status, RebootSubmitStatus::Busy, "new request should be busy while first request is deferred"); - - expectTrue(waitUntil([&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, 1000), - "deferred request should eventually complete"); - - manager.deinit(); + ESPRebootManager manager; + + std::atomic guardCalls{0}; + + ESPRebootManagerConfig cfg{}; + cfg.rebootExecutor = []() {}; + expectTrue(manager.init(cfg), "init should succeed"); + + manager.onRebootRequest([&guardCalls](const RebootRequestContext &) { + const int callNumber = guardCalls.fetch_add(1, std::memory_order_relaxed) + 1; + RebootVote vote{}; + if (callNumber == 1) { + vote.defer = true; + vote.deferTimeoutMs = 80; + std::snprintf(vote.detail, sizeof(vote.detail), "finish write queue"); + } + return vote; + }); + + RebootSubmitResult first = manager.requestReboot("primary", 0); + expectEqual(first.status, RebootSubmitStatus::Queued, "first deferred request should queue"); + + expectTrue( + waitUntil( + [&manager]() { return manager.rebootStatus() == RebootRequestStatus::Deferred; }, + 300 + ), + "manager should expose Deferred status during retry wait" + ); + + RebootSubmitResult second = manager.requestReboot("secondary", 0); + expectEqual( + second.status, + RebootSubmitStatus::Busy, + "new request should be busy while first request is deferred" + ); + + expectTrue( + waitUntil( + [&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, + 1000 + ), + "deferred request should eventually complete" + ); + + manager.deinit(); } void testPollingAndLastEvaluation() { - ESPRebootManager manager; - expectTrue(manager.init(), "init should succeed"); - - manager.onRebootRequest([](const RebootRequestContext&) { - RebootVote vote{}; - vote.allow = false; - std::snprintf(vote.detail, sizeof(vote.detail), "storage flush in progress"); - return vote; - }); - - RebootSubmitResult result = manager.requestReboot("poll-check", 0); - expectEqual(result.status, RebootSubmitStatus::Queued, "poll-check request should queue"); - - expectTrue(manager.isRebootRequested(), "manager should report requested state immediately after queue"); - - expectTrue(waitUntil([&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, 500), - "rejected request should return to idle"); - - RebootEvaluation last = manager.lastEvaluation(); - expectFalse(last.accepted, "last evaluation should report rejection"); - expectEqual(last.code, RebootDecisionCode::Blocked, "last evaluation should preserve blocked code"); - expectTrue(std::strstr(last.detail, "storage") != nullptr, "last evaluation should preserve blocker detail"); - - manager.deinit(); + ESPRebootManager manager; + expectTrue(manager.init(), "init should succeed"); + + manager.onRebootRequest([](const RebootRequestContext &) { + RebootVote vote{}; + vote.allow = false; + std::snprintf(vote.detail, sizeof(vote.detail), "storage flush in progress"); + return vote; + }); + + RebootSubmitResult result = manager.requestReboot("poll-check", 0); + expectEqual(result.status, RebootSubmitStatus::Queued, "poll-check request should queue"); + + expectTrue( + manager.isRebootRequested(), + "manager should report requested state immediately after queue" + ); + + expectTrue( + waitUntil( + [&manager]() { return manager.rebootStatus() == RebootRequestStatus::Idle; }, + 500 + ), + "rejected request should return to idle" + ); + + RebootEvaluation last = manager.lastEvaluation(); + expectFalse(last.accepted, "last evaluation should report rejection"); + expectEqual( + last.code, + RebootDecisionCode::Blocked, + "last evaluation should preserve blocked code" + ); + expectTrue( + std::strstr(last.detail, "storage") != nullptr, + "last evaluation should preserve blocker detail" + ); + + manager.deinit(); } -} // namespace +} // namespace int main() { - try { - testInitDeinitLifecycle(); - testCallbackRegistrationAndUnregister(); - testRequestValidationAndBusyHandling(); - testAcceptedFlowAndRebootExecution(); - testBlockedFlow(); - testCallbackTimeoutFlow(); - testDeferredFlowRetriesFromFirstGuard(); - testDeferredStateRemainsBusyForNewRequests(); - testPollingAndLastEvaluation(); - } catch( const std::exception& exception ){ - std::cerr << "FAIL: " << exception.what() << '\n'; - return 1; - } - - std::cout << "All ESPRebootManager tests passed\n"; - return 0; + try { + testInitDeinitLifecycle(); + testCallbackRegistrationAndUnregister(); + testRequestValidationAndBusyHandling(); + testAcceptedFlowAndRebootExecution(); + testBlockedFlow(); + testCallbackTimeoutFlow(); + testDeferredFlowRetriesFromFirstGuard(); + testDeferredStateRemainsBusyForNewRequests(); + testPollingAndLastEvaluation(); + } catch (const std::exception &exception) { + std::cerr << "FAIL: " << exception.what() << '\n'; + return 1; + } + + std::cout << "All ESPRebootManager tests passed\n"; + return 0; } diff --git a/test/test_support.h b/test/test_support.h index faa3ff6..9badc40 100644 --- a/test/test_support.h +++ b/test/test_support.h @@ -8,4 +8,4 @@ void resetRuntime(); size_t createdTaskCount(); size_t deletedTaskCount(); -} // namespace test_support +} // namespace test_support