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 3089b57..f4a424b 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,13 @@ Stack sizes are expressed in bytes. ## Tests A native host test suite is still being assembled. For now rely on the `examples/` sketches (build with PlatformIO or Arduino IDE) to verify integration, and consider adding regression tests when contributing changes. +## 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 ESPWorker is released under the [MIT License](LICENSE.md). diff --git a/examples/basic_lambda_worker/basic_lambda_worker.ino b/examples/basic_lambda_worker/basic_lambda_worker.ino index 603cb3f..d0bbde8 100644 --- a/examples/basic_lambda_worker/basic_lambda_worker.ino +++ b/examples/basic_lambda_worker/basic_lambda_worker.ino @@ -4,28 +4,30 @@ ESPWorker worker; ESPWorker::Config workerConfig = { - .maxWorkers = 16, - .stackSizeBytes = 2048, - .priority = 1, - .coreId = tskNO_AFFINITY, - .enableExternalStacks = true, + .maxWorkers = 16, + .stackSizeBytes = 2048, + .priority = 1, + .coreId = tskNO_AFFINITY, + .enableExternalStacks = true, }; void setup() { - Serial.begin(115200); - while (!Serial) {} + Serial.begin(115200); + while (!Serial) { + } - worker.init(workerConfig); + worker.init(workerConfig); - // Spawn a default job - auto testJob = worker.spawn([](){ + // Spawn a default job + auto testJob = worker.spawn([]() { Serial.println("[Worker] task is triggered!"); vTaskDelay(pdMS_TO_TICKS(1000)); }); testJob.handler->wait(); // Wait for the job to finish, indefinietly Serial.println("[Worker] task is completed!"); - worker.deinit(); + worker.deinit(); } -void loop() {} +void loop() { +} diff --git a/examples/basic_worker/basic_worker.ino b/examples/basic_worker/basic_worker.ino index 4833f76..56fc389 100644 --- a/examples/basic_worker/basic_worker.ino +++ b/examples/basic_worker/basic_worker.ino @@ -4,30 +4,32 @@ ESPWorker worker; ESPWorker::Config workerConfig = { - .maxWorkers = 16, - .stackSizeBytes = 4096, - .priority = 1, - .coreId = tskNO_AFFINITY, - .enableExternalStacks = true, + .maxWorkers = 16, + .stackSizeBytes = 4096, + .priority = 1, + .coreId = tskNO_AFFINITY, + .enableExternalStacks = true, }; -void jobFunction(){ - Serial.println("[Worker] task is triggered!"); +void jobFunction() { + Serial.println("[Worker] task is triggered!"); vTaskDelay(pdMS_TO_TICKS(1000)); } void setup() { - Serial.begin(115200); - while (!Serial) {} + Serial.begin(115200); + while (!Serial) { + } - worker.init(workerConfig); + worker.init(workerConfig); - // Spawn a default job - auto testJob = worker.spawn(jobFunction); + // Spawn a default job + auto testJob = worker.spawn(jobFunction); testJob.handler->wait(); // Wait for the job to finish, indefinietly Serial.println("[Worker] task is completed!"); - worker.deinit(); + worker.deinit(); } -void loop() {} +void loop() { +} diff --git a/examples/psram_stack/psram_stack.ino b/examples/psram_stack/psram_stack.ino index 40a1bbf..78c577d 100644 --- a/examples/psram_stack/psram_stack.ino +++ b/examples/psram_stack/psram_stack.ino @@ -7,36 +7,38 @@ ESPWorker worker; ESPWorker::Config workerConfig = { - .maxWorkers = 16, - .stackSizeBytes = 2048, - .priority = 1, - .coreId = tskNO_AFFINITY, - .enableExternalStacks = true, + .maxWorkers = 16, + .stackSizeBytes = 2048, + .priority = 1, + .coreId = tskNO_AFFINITY, + .enableExternalStacks = true, }; -void printPSRAM(const char* tag){ - uint32_t freePSRAM = ESP.getFreePsram(); - Serial.printf("[%s] Free PSRAM: %u bytes\n", tag, freePSRAM); +void printPSRAM(const char *tag) { + uint32_t freePSRAM = ESP.getFreePsram(); + Serial.printf("[%s] Free PSRAM: %u bytes\n", tag, freePSRAM); } void setup() { - Serial.begin(115200); - while (!Serial) {} + Serial.begin(115200); + while (!Serial) { + } - worker.init(workerConfig); + worker.init(workerConfig); - printPSRAM("Initial"); + printPSRAM("Initial"); - // Spawn a default job with psram stack - auto testJob = worker.spawnExt([](){ - Serial.println("[Worker] This task stack uses PSRAM!"); - printPSRAM("Inside job"); - }); + // Spawn a default job with psram stack + auto testJob = worker.spawnExt([]() { + Serial.println("[Worker] This task stack uses PSRAM!"); + printPSRAM("Inside job"); + }); - testJob.handler->wait(); // Wait for the job to finish, indefinietly - printPSRAM("After job completed"); + testJob.handler->wait(); // Wait for the job to finish, indefinietly + printPSRAM("After job completed"); - worker.deinit(); + worker.deinit(); } -void loop() {} +void loop() { +} 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_worker/worker.cpp b/src/esp_worker/worker.cpp index c9ec8e3..3a62a89 100644 --- a/src/esp_worker/worker.cpp +++ b/src/esp_worker/worker.cpp @@ -21,7 +21,8 @@ extern "C" { #define ESPWORKER_HAS_IDF_TASK_CAPS 0 #endif -#if ESPWORKER_HAS_IDF_TASK_CAPS && defined(configSUPPORT_STATIC_ALLOCATION) && (configSUPPORT_STATIC_ALLOCATION == 1) && defined(MALLOC_CAP_SPIRAM) +#if ESPWORKER_HAS_IDF_TASK_CAPS && defined(configSUPPORT_STATIC_ALLOCATION) && \ + (configSUPPORT_STATIC_ALLOCATION == 1) && defined(MALLOC_CAP_SPIRAM) #define ESPWORKER_CAN_USE_EXTERNAL_STACKS 1 #else #define ESPWORKER_CAN_USE_EXTERNAL_STACKS 0 @@ -36,530 +37,580 @@ constexpr UBaseType_t kExternalStackCaps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; constexpr UBaseType_t kExternalStackCaps = MALLOC_CAP_8BIT; #endif -template -std::shared_ptr makeInternalShared(Args &&...args) { - void *raw = heap_caps_malloc(sizeof(T), kInternalCaps); - if (!raw) { - return {}; - } - - T *object = new (raw) T(std::forward(args)...); - return std::shared_ptr(object, [](T *ptr) { - if (!ptr) { - return; - } - ptr->~T(); - heap_caps_free(ptr); - }); +template std::shared_ptr makeInternalShared(Args &&...args) { + void *raw = heap_caps_malloc(sizeof(T), kInternalCaps); + if (!raw) { + return {}; + } + + T *object = new (raw) T(std::forward(args)...); + return std::shared_ptr(object, [](T *ptr) { + if (!ptr) { + return; + } + ptr->~T(); + heap_caps_free(ptr); + }); } bool hasExternalStackSupport() { #if ESPWORKER_CAN_USE_EXTERNAL_STACKS - return heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0; + return heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0; #else - return false; + return false; #endif } bool isValidStackConfig(size_t stackBytes) { - if (stackBytes < kMinStackSizeBytes) { - return false; - } - return (stackBytes % sizeof(StackType_t)) == 0; + if (stackBytes < kMinStackSizeBytes) { + return false; + } + return (stackBytes % sizeof(StackType_t)) == 0; } void deleteTaskHandle(TaskHandle_t taskHandle, bool withCaps) { - if (!taskHandle) { - return; - } + if (!taskHandle) { + return; + } #if ESPWORKER_CAN_USE_EXTERNAL_STACKS - if (withCaps) { - vTaskDeleteWithCaps(taskHandle); - return; - } + if (withCaps) { + vTaskDeleteWithCaps(taskHandle); + return; + } #endif - vTaskDelete(taskHandle); + vTaskDelete(taskHandle); } void deleteCurrentTask(bool withCaps) { #if ESPWORKER_CAN_USE_EXTERNAL_STACKS - if (withCaps) { - vTaskDeleteWithCaps(xTaskGetCurrentTaskHandle()); - return; - } + if (withCaps) { + vTaskDeleteWithCaps(xTaskGetCurrentTaskHandle()); + return; + } #endif - vTaskDelete(nullptr); + vTaskDelete(nullptr); } -} // namespace +} // namespace -static_assert(kESPWorkerDefaultStackSizeBytes >= kMinStackSizeBytes, "Default stack size must be at least 1024 bytes."); -static_assert((kESPWorkerDefaultStackSizeBytes % sizeof(StackType_t)) == 0, "Default stack size must be aligned to StackType_t."); +static_assert( + kESPWorkerDefaultStackSizeBytes >= kMinStackSizeBytes, + "Default stack size must be at least 1024 bytes." +); +static_assert( + (kESPWorkerDefaultStackSizeBytes % sizeof(StackType_t)) == 0, + "Default stack size must be aligned to StackType_t." +); struct WorkerHandler::Impl { - ESPWorker *owner{nullptr}; - ESPWorker::TaskCallback callback{}; - WorkerConfig config{}; + ESPWorker *owner{nullptr}; + ESPWorker::TaskCallback callback{}; + WorkerConfig config{}; - TaskHandle_t taskHandle{nullptr}; - TickType_t startTick{0}; - TickType_t endTick{0}; + TaskHandle_t taskHandle{nullptr}; + TickType_t startTick{0}; + TickType_t endTick{0}; - SemaphoreHandle_t completion{nullptr}; - StaticSemaphore_t completionBuffer{}; + SemaphoreHandle_t completion{nullptr}; + StaticSemaphore_t completionBuffer{}; - bool createdWithCaps{false}; + bool createdWithCaps{false}; - std::atomic running{false}; - std::atomic destroyed{false}; - std::atomic finalized{false}; + std::atomic running{false}; + std::atomic destroyed{false}; + std::atomic finalized{false}; - std::weak_ptr self; + std::weak_ptr self; - ~Impl(); + ~Impl(); }; -WorkerHandler::WorkerHandler(std::shared_ptr control) : _control(std::move(control)) {} +WorkerHandler::WorkerHandler(std::shared_ptr control) : _control(std::move(control)) { +} WorkerHandler::Impl::~Impl() { - if (completion) { - vSemaphoreDelete(completion); - completion = nullptr; - } + if (completion) { + vSemaphoreDelete(completion); + completion = nullptr; + } } ESPWorker::~ESPWorker() { - deinit(); + deinit(); } void ESPWorker::deinit() { - std::vector> controls; - { - std::lock_guard guard(_mutex); - _initialized.store(false, std::memory_order_release); - controls.swap(_activeControls); - } - - for (auto &control : controls) { - if (!control) { - continue; - } - - if (control->taskHandle && xTaskGetCurrentTaskHandle() != control->taskHandle) { - deleteTaskHandle(control->taskHandle, control->createdWithCaps); - } - finalizeWorker(control, true); - control->owner = nullptr; - } - - { - std::lock_guard guard(_callbackMutex); - _eventCallback = nullptr; - _errorCallback = nullptr; - } -} - -bool WorkerHandler::valid() const { return static_cast(_control); } - -JobDiag WorkerHandler::getDiag() const { - JobDiag diag{}; - if (!_control) { - return diag; - } - - diag.config = _control->config; - diag.taskHandle = _control->taskHandle; - diag.running = _control->running.load(std::memory_order_acquire); - diag.destroyed = _control->destroyed.load(std::memory_order_acquire); + std::vector> controls; + { + std::lock_guard guard(_mutex); + _initialized.store(false, std::memory_order_release); + controls.swap(_activeControls); + } + + for (auto &control : controls) { + if (!control) { + continue; + } + + if (control->taskHandle && xTaskGetCurrentTaskHandle() != control->taskHandle) { + deleteTaskHandle(control->taskHandle, control->createdWithCaps); + } + finalizeWorker(control, true); + control->owner = nullptr; + } + + { + std::lock_guard guard(_callbackMutex); + _eventCallback = nullptr; + _errorCallback = nullptr; + } +} - TickType_t endTicks = diag.running ? xTaskGetTickCount() : _control->endTick; - if (endTicks >= _control->startTick) { - TickType_t elapsedTicks = endTicks - _control->startTick; - diag.runtimeMs = static_cast(elapsedTicks * portTICK_PERIOD_MS); - } +bool WorkerHandler::valid() const { + return static_cast(_control); +} - return diag; +JobDiag WorkerHandler::getDiag() const { + JobDiag diag{}; + if (!_control) { + return diag; + } + + diag.config = _control->config; + diag.taskHandle = _control->taskHandle; + diag.running = _control->running.load(std::memory_order_acquire); + diag.destroyed = _control->destroyed.load(std::memory_order_acquire); + + TickType_t endTicks = diag.running ? xTaskGetTickCount() : _control->endTick; + if (endTicks >= _control->startTick) { + TickType_t elapsedTicks = endTicks - _control->startTick; + diag.runtimeMs = static_cast(elapsedTicks * portTICK_PERIOD_MS); + } + + return diag; } bool WorkerHandler::wait(TickType_t ticks) { - if (!_control) { - return false; - } - std::shared_ptr control = _control; - if (!control->completion) { - return false; - } - - if (!control->running.load(std::memory_order_acquire)) { - return true; - } - - if (xSemaphoreTake(control->completion, ticks) == pdTRUE) { - return true; - } - - return !control->running.load(std::memory_order_acquire); + if (!_control) { + return false; + } + std::shared_ptr control = _control; + if (!control->completion) { + return false; + } + + if (!control->running.load(std::memory_order_acquire)) { + return true; + } + + if (xSemaphoreTake(control->completion, ticks) == pdTRUE) { + return true; + } + + return !control->running.load(std::memory_order_acquire); } bool WorkerHandler::destroy() { - if (!_control) { - return false; - } - std::shared_ptr control = _control; - if (!control->owner) { - return false; - } - return control->owner->destroyWorker(control); + if (!_control) { + return false; + } + std::shared_ptr control = _control; + if (!control->owner) { + return false; + } + return control->owner->destroyWorker(control); } void ESPWorker::init(const Config &config) { - std::lock_guard guard(_mutex); - _config = config; - _initialized.store(true, std::memory_order_release); + std::lock_guard guard(_mutex); + _config = config; + _initialized.store(true, std::memory_order_release); } WorkerResult ESPWorker::spawn(TaskCallback callback, const WorkerConfig &config) { - if (!_initialized) { - init(Config{}); - } - WorkerConfig effective = config; - if (effective.stackSizeBytes == 0) { - effective.stackSizeBytes = _config.stackSizeBytes; - } - if (effective.priority == 0) { - effective.priority = _config.priority; - } - if (effective.coreId == tskNO_AFFINITY) { - effective.coreId = _config.coreId; - } - if (effective.name.empty()) { - effective.name = makeName(); - } - - return spawnInternal(std::move(callback), std::move(effective)); + if (!_initialized) { + init(Config{}); + } + WorkerConfig effective = config; + if (effective.stackSizeBytes == 0) { + effective.stackSizeBytes = _config.stackSizeBytes; + } + if (effective.priority == 0) { + effective.priority = _config.priority; + } + if (effective.coreId == tskNO_AFFINITY) { + effective.coreId = _config.coreId; + } + if (effective.name.empty()) { + effective.name = makeName(); + } + + return spawnInternal(std::move(callback), std::move(effective)); } WorkerResult ESPWorker::spawnExt(TaskCallback callback, const WorkerConfig &config) { - WorkerConfig extConfig = config; - extConfig.useExternalStack = true; - return spawn(std::move(callback), extConfig); + WorkerConfig extConfig = config; + extConfig.useExternalStack = true; + return spawn(std::move(callback), extConfig); } WorkerResult ESPWorker::spawnInternal(TaskCallback &&callback, WorkerConfig config) { - if (!callback) { - notifyError(WorkerError::InvalidConfig); - return {WorkerError::InvalidConfig, {}, "Callback must be callable"}; - } - - if (!isValidStackConfig(config.stackSizeBytes)) { - notifyError(WorkerError::InvalidConfig); - return {WorkerError::InvalidConfig, {}, "stackSizeBytes must be >= 1024 and aligned to StackType_t"}; - } - - if (config.useExternalStack) { - if (!_config.enableExternalStacks) { - notifyError(WorkerError::ExternalStackUnsupported); - return {WorkerError::ExternalStackUnsupported, {}, "External stacks are disabled in ESPWorker::Config"}; - } - if (!hasExternalStackSupport()) { - notifyError(WorkerError::ExternalStackUnsupported); - return {WorkerError::ExternalStackUnsupported, {}, "External stack mode is not supported on this target"}; - } - } - - auto control = makeInternalShared(); - if (!control) { - notifyError(WorkerError::NoMemory); - return {WorkerError::NoMemory, {}, "Failed to allocate worker control in internal RAM"}; - } - control->owner = this; - control->callback = std::move(callback); - control->config = std::move(config); - - control->completion = xSemaphoreCreateBinaryStatic(&control->completionBuffer); - if (!control->completion) { - notifyError(WorkerError::NoMemory); - return {WorkerError::NoMemory, {}, "Failed to create completion semaphore"}; - } - - control->self = control; - - bool limitReached = false; - { - std::lock_guard guard(_mutex); - if (_activeControls.size() >= _config.maxWorkers) { - limitReached = true; - } else { - _activeControls.push_back(control); - } - } - - if (limitReached) { - notifyError(WorkerError::MaxWorkersReached); - return {WorkerError::MaxWorkersReached, {}, "Maximum workers reached"}; - } - - const size_t stackBytes = control->config.stackSizeBytes; - BaseType_t createResult = pdFAIL; - - if (control->config.useExternalStack) { + if (!callback) { + notifyError(WorkerError::InvalidConfig); + return {WorkerError::InvalidConfig, {}, "Callback must be callable"}; + } + + if (!isValidStackConfig(config.stackSizeBytes)) { + notifyError(WorkerError::InvalidConfig); + return { + WorkerError::InvalidConfig, + {}, + "stackSizeBytes must be >= 1024 and aligned to StackType_t" + }; + } + + if (config.useExternalStack) { + if (!_config.enableExternalStacks) { + notifyError(WorkerError::ExternalStackUnsupported); + return { + WorkerError::ExternalStackUnsupported, + {}, + "External stacks are disabled in ESPWorker::Config" + }; + } + if (!hasExternalStackSupport()) { + notifyError(WorkerError::ExternalStackUnsupported); + return { + WorkerError::ExternalStackUnsupported, + {}, + "External stack mode is not supported on this target" + }; + } + } + + auto control = makeInternalShared(); + if (!control) { + notifyError(WorkerError::NoMemory); + return {WorkerError::NoMemory, {}, "Failed to allocate worker control in internal RAM"}; + } + control->owner = this; + control->callback = std::move(callback); + control->config = std::move(config); + + control->completion = xSemaphoreCreateBinaryStatic(&control->completionBuffer); + if (!control->completion) { + notifyError(WorkerError::NoMemory); + return {WorkerError::NoMemory, {}, "Failed to create completion semaphore"}; + } + + control->self = control; + + bool limitReached = false; + { + std::lock_guard guard(_mutex); + if (_activeControls.size() >= _config.maxWorkers) { + limitReached = true; + } else { + _activeControls.push_back(control); + } + } + + if (limitReached) { + notifyError(WorkerError::MaxWorkersReached); + return {WorkerError::MaxWorkersReached, {}, "Maximum workers reached"}; + } + + const size_t stackBytes = control->config.stackSizeBytes; + BaseType_t createResult = pdFAIL; + + if (control->config.useExternalStack) { #if ESPWORKER_CAN_USE_EXTERNAL_STACKS - createResult = xTaskCreatePinnedToCoreWithCaps(taskTrampoline, - control->config.name.c_str(), - static_cast(stackBytes), - control.get(), - control->config.priority, - &control->taskHandle, - control->config.coreId, - kExternalStackCaps); - control->createdWithCaps = (createResult == pdPASS); + createResult = xTaskCreatePinnedToCoreWithCaps( + taskTrampoline, + control->config.name.c_str(), + static_cast(stackBytes), + control.get(), + control->config.priority, + &control->taskHandle, + control->config.coreId, + kExternalStackCaps + ); + control->createdWithCaps = (createResult == pdPASS); #else - createResult = pdFAIL; + createResult = pdFAIL; #endif - } else { - createResult = xTaskCreatePinnedToCore(taskTrampoline, control->config.name.c_str(), static_cast(stackBytes), control.get(), control->config.priority, &control->taskHandle, control->config.coreId); - control->createdWithCaps = false; - } - - if (createResult != pdPASS) { - { - std::lock_guard guard(_mutex); - _activeControls.erase(std::remove_if(_activeControls.begin(), _activeControls.end(), [&](const auto &ptr) { return ptr.get() == control.get(); }), _activeControls.end()); - } - - notifyError(WorkerError::TaskCreateFailed); - return {WorkerError::TaskCreateFailed, {}, "Failed to create worker task"}; - } - - control->running.store(true, std::memory_order_release); - control->startTick = xTaskGetTickCount(); - - auto handler = std::shared_ptr(new WorkerHandler(control)); - notifyEvent(WorkerEvent::Created); - return {WorkerError::None, handler, nullptr}; + } else { + createResult = xTaskCreatePinnedToCore( + taskTrampoline, + control->config.name.c_str(), + static_cast(stackBytes), + control.get(), + control->config.priority, + &control->taskHandle, + control->config.coreId + ); + control->createdWithCaps = false; + } + + if (createResult != pdPASS) { + { + std::lock_guard guard(_mutex); + _activeControls.erase( + std::remove_if( + _activeControls.begin(), + _activeControls.end(), + [&](const auto &ptr) { return ptr.get() == control.get(); } + ), + _activeControls.end() + ); + } + + notifyError(WorkerError::TaskCreateFailed); + return {WorkerError::TaskCreateFailed, {}, "Failed to create worker task"}; + } + + control->running.store(true, std::memory_order_release); + control->startTick = xTaskGetTickCount(); + + auto handler = std::shared_ptr(new WorkerHandler(control)); + notifyEvent(WorkerEvent::Created); + return {WorkerError::None, handler, nullptr}; } void ESPWorker::taskTrampoline(void *arg) { - auto *controlPtr = static_cast(arg); - if (!controlPtr) { - vTaskDelete(nullptr); - return; - } - const bool createdWithCaps = controlPtr->createdWithCaps; - - std::shared_ptr control = controlPtr->self.lock(); - if (!control || !control->owner) { - vTaskDelete(nullptr); - return; - } - - control->owner->notifyEvent(WorkerEvent::Started); - control->owner->runTask(std::move(control)); - deleteCurrentTask(createdWithCaps); + auto *controlPtr = static_cast(arg); + if (!controlPtr) { + vTaskDelete(nullptr); + return; + } + const bool createdWithCaps = controlPtr->createdWithCaps; + + std::shared_ptr control = controlPtr->self.lock(); + if (!control || !control->owner) { + vTaskDelete(nullptr); + return; + } + + control->owner->notifyEvent(WorkerEvent::Started); + control->owner->runTask(std::move(control)); + deleteCurrentTask(createdWithCaps); } void ESPWorker::runTask(std::shared_ptr control) { - auto callback = std::move(control->callback); - if (callback) { - callback(); - } - finalizeWorker(control, false); + auto callback = std::move(control->callback); + if (callback) { + callback(); + } + finalizeWorker(control, false); } -void ESPWorker::finalizeWorker(const std::shared_ptr &control, bool destroyed) { - if (!control) { - return; - } - - bool expected = false; - if (!control->finalized.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { - return; - } - - control->destroyed.store(destroyed, std::memory_order_release); - control->running.store(false, std::memory_order_release); - control->endTick = xTaskGetTickCount(); - - if (control->taskHandle) { - control->taskHandle = nullptr; - } - - if (control->completion) { - xSemaphoreGive(control->completion); - } - - { - std::lock_guard guard(_mutex); - _activeControls.erase(std::remove_if(_activeControls.begin(), _activeControls.end(), [&](const auto &ptr) { return ptr.get() == control.get(); }), _activeControls.end()); - } - - notifyEvent(destroyed ? WorkerEvent::Destroyed : WorkerEvent::Completed); +void ESPWorker::finalizeWorker( + const std::shared_ptr &control, bool destroyed +) { + if (!control) { + return; + } + + bool expected = false; + if (!control->finalized.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { + return; + } + + control->destroyed.store(destroyed, std::memory_order_release); + control->running.store(false, std::memory_order_release); + control->endTick = xTaskGetTickCount(); + + if (control->taskHandle) { + control->taskHandle = nullptr; + } + + if (control->completion) { + xSemaphoreGive(control->completion); + } + + { + std::lock_guard guard(_mutex); + _activeControls.erase( + std::remove_if( + _activeControls.begin(), + _activeControls.end(), + [&](const auto &ptr) { return ptr.get() == control.get(); } + ), + _activeControls.end() + ); + } + + notifyEvent(destroyed ? WorkerEvent::Destroyed : WorkerEvent::Completed); } bool ESPWorker::destroyWorker(const std::shared_ptr &control) { - if (!control) { - return false; - } - - if (!control->running.load(std::memory_order_acquire)) { - return true; - } - - if (control->taskHandle == nullptr) { - finalizeWorker(control, true); - return true; - } - - if (xTaskGetCurrentTaskHandle() == control->taskHandle) { - notifyError(WorkerError::InvalidConfig); - return false; - } - - deleteTaskHandle(control->taskHandle, control->createdWithCaps); - finalizeWorker(control, true); - return true; + if (!control) { + return false; + } + + if (!control->running.load(std::memory_order_acquire)) { + return true; + } + + if (control->taskHandle == nullptr) { + finalizeWorker(control, true); + return true; + } + + if (xTaskGetCurrentTaskHandle() == control->taskHandle) { + notifyError(WorkerError::InvalidConfig); + return false; + } + + deleteTaskHandle(control->taskHandle, control->createdWithCaps); + finalizeWorker(control, true); + return true; } size_t ESPWorker::activeWorkers() const { - std::lock_guard guard(_mutex); - return _activeControls.size(); + std::lock_guard guard(_mutex); + return _activeControls.size(); } void ESPWorker::cleanupFinished() { - std::lock_guard guard(_mutex); - _activeControls.erase(std::remove_if(_activeControls.begin(), _activeControls.end(), [](const auto &ptr) { - return !ptr || !ptr->running.load(std::memory_order_acquire); - }), - _activeControls.end()); + std::lock_guard guard(_mutex); + _activeControls.erase( + std::remove_if( + _activeControls.begin(), + _activeControls.end(), + [](const auto &ptr) { return !ptr || !ptr->running.load(std::memory_order_acquire); } + ), + _activeControls.end() + ); } WorkerDiag ESPWorker::getDiag() const { - WorkerDiag diag{}; - - std::vector> activeControls; - { - std::lock_guard guard(_mutex); - activeControls = _activeControls; - } - - diag.totalJobs = activeControls.size(); - if (activeControls.empty()) { - return diag; - } - - TickType_t now = xTaskGetTickCount(); - uint64_t runtimeSum = 0; - bool haveRuntime = false; - - for (const auto &control : activeControls) { - if (!control) { - continue; - } - - bool running = control->running.load(std::memory_order_acquire); - if (running) { - diag.runningJobs++; - } - - if (control->config.useExternalStack) { - diag.psramStackJobs++; - } - - TickType_t endTicks = running ? now : control->endTick; - if (endTicks >= control->startTick) { - TickType_t elapsedTicks = endTicks - control->startTick; - uint32_t runtimeMs = static_cast(elapsedTicks * portTICK_PERIOD_MS); - runtimeSum += runtimeMs; - diag.maxRuntimeMs = std::max(diag.maxRuntimeMs, runtimeMs); - haveRuntime = true; - } - } - - if (diag.totalJobs > diag.runningJobs) { - diag.waitingJobs = diag.totalJobs - diag.runningJobs; - } - - if (diag.totalJobs > 0 && haveRuntime) { - diag.averageRuntimeMs = static_cast(runtimeSum / diag.totalJobs); - } - - return diag; + WorkerDiag diag{}; + + std::vector> activeControls; + { + std::lock_guard guard(_mutex); + activeControls = _activeControls; + } + + diag.totalJobs = activeControls.size(); + if (activeControls.empty()) { + return diag; + } + + TickType_t now = xTaskGetTickCount(); + uint64_t runtimeSum = 0; + bool haveRuntime = false; + + for (const auto &control : activeControls) { + if (!control) { + continue; + } + + bool running = control->running.load(std::memory_order_acquire); + if (running) { + diag.runningJobs++; + } + + if (control->config.useExternalStack) { + diag.psramStackJobs++; + } + + TickType_t endTicks = running ? now : control->endTick; + if (endTicks >= control->startTick) { + TickType_t elapsedTicks = endTicks - control->startTick; + uint32_t runtimeMs = static_cast(elapsedTicks * portTICK_PERIOD_MS); + runtimeSum += runtimeMs; + diag.maxRuntimeMs = std::max(diag.maxRuntimeMs, runtimeMs); + haveRuntime = true; + } + } + + if (diag.totalJobs > diag.runningJobs) { + diag.waitingJobs = diag.totalJobs - diag.runningJobs; + } + + if (diag.totalJobs > 0 && haveRuntime) { + diag.averageRuntimeMs = static_cast(runtimeSum / diag.totalJobs); + } + + return diag; } void ESPWorker::onEvent(EventCallback callback) { - std::lock_guard guard(_callbackMutex); - _eventCallback = std::move(callback); + std::lock_guard guard(_callbackMutex); + _eventCallback = std::move(callback); } void ESPWorker::onError(ErrorCallback callback) { - std::lock_guard guard(_callbackMutex); - _errorCallback = std::move(callback); + std::lock_guard guard(_callbackMutex); + _errorCallback = std::move(callback); } const char *ESPWorker::eventToString(WorkerEvent event) const { - switch (event) { - case WorkerEvent::Created: - return "Created"; - case WorkerEvent::Started: - return "Started"; - case WorkerEvent::Completed: - return "Completed"; - case WorkerEvent::Destroyed: - return "Destroyed"; - default: - return "Unknown"; - } + switch (event) { + case WorkerEvent::Created: + return "Created"; + case WorkerEvent::Started: + return "Started"; + case WorkerEvent::Completed: + return "Completed"; + case WorkerEvent::Destroyed: + return "Destroyed"; + default: + return "Unknown"; + } } const char *ESPWorker::errorToString(WorkerError error) const { - switch (error) { - case WorkerError::None: - return "None"; - case WorkerError::NotInitialized: - return "NotInitialized"; - case WorkerError::InvalidConfig: - return "InvalidConfig"; - case WorkerError::MaxWorkersReached: - return "MaxWorkersReached"; - case WorkerError::TaskCreateFailed: - return "TaskCreateFailed"; - case WorkerError::NoMemory: - return "NoMemory"; - case WorkerError::ExternalStackUnsupported: - return "ExternalStackUnsupported"; - default: - return "Unknown"; - } + switch (error) { + case WorkerError::None: + return "None"; + case WorkerError::NotInitialized: + return "NotInitialized"; + case WorkerError::InvalidConfig: + return "InvalidConfig"; + case WorkerError::MaxWorkersReached: + return "MaxWorkersReached"; + case WorkerError::TaskCreateFailed: + return "TaskCreateFailed"; + case WorkerError::NoMemory: + return "NoMemory"; + case WorkerError::ExternalStackUnsupported: + return "ExternalStackUnsupported"; + default: + return "Unknown"; + } } std::string ESPWorker::makeName() { - static std::atomic counter{0}; - uint32_t id = counter.fetch_add(1, std::memory_order_relaxed); - char buffer[24]; - snprintf(buffer, sizeof(buffer), "worker-%u", id); - return std::string(buffer); + static std::atomic counter{0}; + uint32_t id = counter.fetch_add(1, std::memory_order_relaxed); + char buffer[24]; + snprintf(buffer, sizeof(buffer), "worker-%u", id); + return std::string(buffer); } void ESPWorker::notifyEvent(WorkerEvent event) { - EventCallback callback; - { - std::lock_guard guard(_callbackMutex); - callback = _eventCallback; - } - if (callback) { - callback(event); - } + EventCallback callback; + { + std::lock_guard guard(_callbackMutex); + callback = _eventCallback; + } + if (callback) { + callback(event); + } } void ESPWorker::notifyError(WorkerError error) { - if (error == WorkerError::None) { - return; - } - ErrorCallback callback; - { - std::lock_guard guard(_callbackMutex); - callback = _errorCallback; - } - if (callback) { - callback(error); - } + if (error == WorkerError::None) { + return; + } + ErrorCallback callback; + { + std::lock_guard guard(_callbackMutex); + callback = _errorCallback; + } + if (callback) { + callback(error); + } } diff --git a/src/esp_worker/worker.h b/src/esp_worker/worker.h index 6b779a3..187a727 100644 --- a/src/esp_worker/worker.h +++ b/src/esp_worker/worker.h @@ -21,127 +21,131 @@ class ESPWorker; constexpr size_t kESPWorkerDefaultStackSizeBytes = 4096; struct WorkerConfig { - size_t stackSizeBytes = kESPWorkerDefaultStackSizeBytes; // Task stack size in bytes - UBaseType_t priority = 1; // FreeRTOS task priority - BaseType_t coreId = tskNO_AFFINITY; // preferred core, or tskNO_AFFINITY for any - std::string name{}; // optional task name - bool useExternalStack = false; // request PSRAM backed stack for the task + size_t stackSizeBytes = kESPWorkerDefaultStackSizeBytes; // Task stack size in bytes + UBaseType_t priority = 1; // FreeRTOS task priority + BaseType_t coreId = tskNO_AFFINITY; // preferred core, or tskNO_AFFINITY for any + std::string name{}; // optional task name + bool useExternalStack = false; // request PSRAM backed stack for the task }; struct JobDiag { - WorkerConfig config{}; - uint32_t runtimeMs = 0; - bool running = false; - bool destroyed = false; - TaskHandle_t taskHandle = nullptr; + WorkerConfig config{}; + uint32_t runtimeMs = 0; + bool running = false; + bool destroyed = false; + TaskHandle_t taskHandle = nullptr; }; struct WorkerDiag { - size_t totalJobs = 0; - size_t runningJobs = 0; - size_t waitingJobs = 0; - size_t psramStackJobs = 0; - uint32_t averageRuntimeMs = 0; - uint32_t maxRuntimeMs = 0; + size_t totalJobs = 0; + size_t runningJobs = 0; + size_t waitingJobs = 0; + size_t psramStackJobs = 0; + uint32_t averageRuntimeMs = 0; + uint32_t maxRuntimeMs = 0; }; enum class WorkerError { - None = 0, - NotInitialized, - InvalidConfig, - MaxWorkersReached, - TaskCreateFailed, - NoMemory, - ExternalStackUnsupported, + None = 0, + NotInitialized, + InvalidConfig, + MaxWorkersReached, + TaskCreateFailed, + NoMemory, + ExternalStackUnsupported, }; enum class WorkerEvent { - Created = 0, - Started, - Completed, - Destroyed, + Created = 0, + Started, + Completed, + Destroyed, }; class WorkerHandler { - public: - WorkerHandler() = default; + public: + WorkerHandler() = default; - bool valid() const; - JobDiag getDiag() const; - bool wait(TickType_t ticks = portMAX_DELAY); - bool destroy(); + bool valid() const; + JobDiag getDiag() const; + bool wait(TickType_t ticks = portMAX_DELAY); + bool destroy(); - private: - struct Impl; - friend class ESPWorker; - explicit WorkerHandler(std::shared_ptr control); + private: + struct Impl; + friend class ESPWorker; + explicit WorkerHandler(std::shared_ptr control); - std::shared_ptr _control{}; + std::shared_ptr _control{}; }; struct WorkerResult { - WorkerError error{WorkerError::None}; - std::shared_ptr handler{}; - const char *message{nullptr}; + WorkerError error{WorkerError::None}; + std::shared_ptr handler{}; + const char *message{nullptr}; - explicit operator bool() const { return error == WorkerError::None; } + explicit operator bool() const { + return error == WorkerError::None; + } }; class ESPWorker { - friend class WorkerHandler; + friend class WorkerHandler; - public: - using TaskCallback = std::function; - using EventCallback = std::function; - using ErrorCallback = std::function; + public: + using TaskCallback = std::function; + using EventCallback = std::function; + using ErrorCallback = std::function; - struct Config { - size_t maxWorkers = 8; - size_t stackSizeBytes = kESPWorkerDefaultStackSizeBytes; - UBaseType_t priority = 1; - BaseType_t coreId = tskNO_AFFINITY; - bool enableExternalStacks = true; - }; + struct Config { + size_t maxWorkers = 8; + size_t stackSizeBytes = kESPWorkerDefaultStackSizeBytes; + UBaseType_t priority = 1; + BaseType_t coreId = tskNO_AFFINITY; + bool enableExternalStacks = true; + }; - ESPWorker() = default; - ~ESPWorker(); + ESPWorker() = default; + ~ESPWorker(); - void init(const Config &config); - void deinit(); - bool isInitialized() const { return _initialized.load(std::memory_order_acquire); } + void init(const Config &config); + void deinit(); + bool isInitialized() const { + return _initialized.load(std::memory_order_acquire); + } - WorkerResult spawn(TaskCallback callback, const WorkerConfig &config = WorkerConfig{}); - WorkerResult spawnExt(TaskCallback callback, const WorkerConfig &config = WorkerConfig{}); + WorkerResult spawn(TaskCallback callback, const WorkerConfig &config = WorkerConfig{}); + WorkerResult spawnExt(TaskCallback callback, const WorkerConfig &config = WorkerConfig{}); - size_t activeWorkers() const; - void cleanupFinished(); + size_t activeWorkers() const; + void cleanupFinished(); - WorkerDiag getDiag() const; + WorkerDiag getDiag() const; - void onEvent(EventCallback callback); - void onError(ErrorCallback callback); + void onEvent(EventCallback callback); + void onError(ErrorCallback callback); - const char *eventToString(WorkerEvent event) const; - const char *errorToString(WorkerError error) const; + const char *eventToString(WorkerEvent event) const; + const char *errorToString(WorkerError error) const; - private: - WorkerResult spawnInternal(TaskCallback &&callback, WorkerConfig config); - static void taskTrampoline(void *arg); + private: + WorkerResult spawnInternal(TaskCallback &&callback, WorkerConfig config); + static void taskTrampoline(void *arg); - void runTask(std::shared_ptr control); - void finalizeWorker(const std::shared_ptr &control, bool destroyed); - bool destroyWorker(const std::shared_ptr &control); - std::string makeName(); - void notifyEvent(WorkerEvent event); - void notifyError(WorkerError error); + void runTask(std::shared_ptr control); + void finalizeWorker(const std::shared_ptr &control, bool destroyed); + bool destroyWorker(const std::shared_ptr &control); + std::string makeName(); + void notifyEvent(WorkerEvent event); + void notifyError(WorkerError error); - Config _config{}; - std::atomic _initialized{false}; + Config _config{}; + std::atomic _initialized{false}; - mutable std::mutex _mutex; - std::vector> _activeControls; + mutable std::mutex _mutex; + std::vector> _activeControls; - mutable std::mutex _callbackMutex; - EventCallback _eventCallback{}; - ErrorCallback _errorCallback{}; + mutable std::mutex _callbackMutex; + EventCallback _eventCallback{}; + ErrorCallback _errorCallback{}; }; diff --git a/test/esp_worker_lifecycle_tests.cpp b/test/esp_worker_lifecycle_tests.cpp index acf1538..e84d102 100644 --- a/test/esp_worker_lifecycle_tests.cpp +++ b/test/esp_worker_lifecycle_tests.cpp @@ -10,130 +10,178 @@ namespace { [[noreturn]] void fail(const std::string &message) { - throw std::runtime_error(message); + throw std::runtime_error(message); } void expectTrue(bool condition, const std::string &message) { - if (!condition) { - fail(message); - } + if (!condition) { + fail(message); + } } void expectFalse(bool condition, const std::string &message) { - if (condition) { - fail(message); - } + if (condition) { + fail(message); + } } template void expectEqual(const T &actual, const T &expected, const std::string &message) { - if (!(actual == expected)) { - fail(message); - } + if (!(actual == expected)) { + fail(message); + } } void testDeinitIsSafeBeforeInit() { - ESPWorker worker; - expectFalse(worker.isInitialized(), "worker should start deinitialized"); - expectEqual(worker.activeWorkers(), static_cast(0), "worker should start with no active jobs"); - - worker.deinit(); - expectFalse(worker.isInitialized(), "deinit before init should be a no-op"); - expectEqual(worker.activeWorkers(), static_cast(0), "deinit before init should keep worker count at zero"); + ESPWorker worker; + expectFalse(worker.isInitialized(), "worker should start deinitialized"); + expectEqual( + worker.activeWorkers(), + static_cast(0), + "worker should start with no active jobs" + ); + + worker.deinit(); + expectFalse(worker.isInitialized(), "deinit before init should be a no-op"); + expectEqual( + worker.activeWorkers(), + static_cast(0), + "deinit before init should keep worker count at zero" + ); } void testDeinitIsIdempotent() { - ESPWorker worker; - ESPWorker::Config cfg{}; - cfg.maxWorkers = 4; - worker.init(cfg); - - expectTrue(worker.isInitialized(), "worker should be initialized"); - - worker.deinit(); - expectFalse(worker.isInitialized(), "worker should deinitialize cleanly"); - expectEqual(worker.activeWorkers(), static_cast(0), "deinit should clear worker state"); - - worker.deinit(); - expectFalse(worker.isInitialized(), "second deinit should remain safe"); - expectEqual(worker.activeWorkers(), static_cast(0), "second deinit should not reintroduce workers"); + ESPWorker worker; + ESPWorker::Config cfg{}; + cfg.maxWorkers = 4; + worker.init(cfg); + + expectTrue(worker.isInitialized(), "worker should be initialized"); + + worker.deinit(); + expectFalse(worker.isInitialized(), "worker should deinitialize cleanly"); + expectEqual(worker.activeWorkers(), static_cast(0), "deinit should clear worker state"); + + worker.deinit(); + expectFalse(worker.isInitialized(), "second deinit should remain safe"); + expectEqual( + worker.activeWorkers(), + static_cast(0), + "second deinit should not reintroduce workers" + ); } void testReinitLifecycleAfterDeinit() { - test_support::resetRuntime(); - - ESPWorker worker; - ESPWorker::Config cfg{}; - cfg.maxWorkers = 2; - worker.init(cfg); - expectTrue(worker.isInitialized(), "first init should succeed"); - - WorkerResult first = worker.spawn([]() {}); - expectTrue(static_cast(first), "spawn after first init should succeed"); - expectEqual(worker.activeWorkers(), static_cast(1), "first spawn should reserve one worker slot"); - - worker.deinit(); - expectFalse(worker.isInitialized(), "worker should be deinitialized after first lifecycle"); - expectEqual(worker.activeWorkers(), static_cast(0), "deinit should clear active workers"); - - worker.init(cfg); - expectTrue(worker.isInitialized(), "second init should succeed"); - - WorkerResult second = worker.spawn([]() {}); - expectTrue(static_cast(second), "spawn after reinit should succeed"); - expectEqual(worker.activeWorkers(), static_cast(1), "second spawn should reserve one worker slot"); - - worker.deinit(); + test_support::resetRuntime(); + + ESPWorker worker; + ESPWorker::Config cfg{}; + cfg.maxWorkers = 2; + worker.init(cfg); + expectTrue(worker.isInitialized(), "first init should succeed"); + + WorkerResult first = worker.spawn([]() {}); + expectTrue(static_cast(first), "spawn after first init should succeed"); + expectEqual( + worker.activeWorkers(), + static_cast(1), + "first spawn should reserve one worker slot" + ); + + worker.deinit(); + expectFalse(worker.isInitialized(), "worker should be deinitialized after first lifecycle"); + expectEqual( + worker.activeWorkers(), + static_cast(0), + "deinit should clear active workers" + ); + + worker.init(cfg); + expectTrue(worker.isInitialized(), "second init should succeed"); + + WorkerResult second = worker.spawn([]() {}); + expectTrue(static_cast(second), "spawn after reinit should succeed"); + expectEqual( + worker.activeWorkers(), + static_cast(1), + "second spawn should reserve one worker slot" + ); + + worker.deinit(); } void testDeinitReleasesActiveTaskHandles() { - test_support::resetRuntime(); - - ESPWorker worker; - worker.init(ESPWorker::Config{}); - WorkerResult first = worker.spawn([]() {}); - WorkerResult second = worker.spawn([]() {}); - - expectTrue(static_cast(first), "first spawn should succeed"); - expectTrue(static_cast(second), "second spawn should succeed"); - expectEqual(worker.activeWorkers(), static_cast(2), "two workers should be active before teardown"); - expectEqual(test_support::createdTaskCount(), static_cast(2), "stubs should observe two created tasks"); - - worker.deinit(); - - expectFalse(worker.isInitialized(), "worker should be deinitialized"); - expectEqual(worker.activeWorkers(), static_cast(0), "deinit should clear active workers"); - expectEqual(test_support::deletedTaskCount(), static_cast(2), "deinit should release all active task handles"); + test_support::resetRuntime(); + + ESPWorker worker; + worker.init(ESPWorker::Config{}); + WorkerResult first = worker.spawn([]() {}); + WorkerResult second = worker.spawn([]() {}); + + expectTrue(static_cast(first), "first spawn should succeed"); + expectTrue(static_cast(second), "second spawn should succeed"); + expectEqual( + worker.activeWorkers(), + static_cast(2), + "two workers should be active before teardown" + ); + expectEqual( + test_support::createdTaskCount(), + static_cast(2), + "stubs should observe two created tasks" + ); + + worker.deinit(); + + expectFalse(worker.isInitialized(), "worker should be deinitialized"); + expectEqual( + worker.activeWorkers(), + static_cast(0), + "deinit should clear active workers" + ); + expectEqual( + test_support::deletedTaskCount(), + static_cast(2), + "deinit should release all active task handles" + ); } void testDestructorDelegatesToDeinit() { - test_support::resetRuntime(); - - { - ESPWorker worker; - worker.init(ESPWorker::Config{}); - WorkerResult result = worker.spawn([]() {}); - expectTrue(static_cast(result), "spawn should succeed for destructor test"); - expectEqual(worker.activeWorkers(), static_cast(1), "one worker should be active before scope exit"); - } - - expectEqual(test_support::deletedTaskCount(), static_cast(1), "destructor should deinit and release active worker"); + test_support::resetRuntime(); + + { + ESPWorker worker; + worker.init(ESPWorker::Config{}); + WorkerResult result = worker.spawn([]() {}); + expectTrue(static_cast(result), "spawn should succeed for destructor test"); + expectEqual( + worker.activeWorkers(), + static_cast(1), + "one worker should be active before scope exit" + ); + } + + expectEqual( + test_support::deletedTaskCount(), + static_cast(1), + "destructor should deinit and release active worker" + ); } -} // namespace +} // namespace int main() { - try { - testDeinitIsSafeBeforeInit(); - testDeinitIsIdempotent(); - testReinitLifecycleAfterDeinit(); - testDeinitReleasesActiveTaskHandles(); - testDestructorDelegatesToDeinit(); - } catch (const std::exception &ex) { - std::cerr << "FAIL: " << ex.what() << '\n'; - return 1; - } - - std::cout << "All esp-worker lifecycle tests passed\n"; - return 0; + try { + testDeinitIsSafeBeforeInit(); + testDeinitIsIdempotent(); + testReinitLifecycleAfterDeinit(); + testDeinitReleasesActiveTaskHandles(); + testDestructorDelegatesToDeinit(); + } catch (const std::exception &ex) { + std::cerr << "FAIL: " << ex.what() << '\n'; + return 1; + } + + std::cout << "All esp-worker lifecycle tests passed\n"; + return 0; } diff --git a/test/stubs/freertos/FreeRTOS.h b/test/stubs/freertos/FreeRTOS.h index eb6766c..03f7ed7 100644 --- a/test/stubs/freertos/FreeRTOS.h +++ b/test/stubs/freertos/FreeRTOS.h @@ -16,7 +16,7 @@ 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/task.h b/test/stubs/freertos/task.h index 3540841..e7f7ef4 100644 --- a/test/stubs/freertos/task.h +++ b/test/stubs/freertos/task.h @@ -8,13 +8,15 @@ extern "C" { 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_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 diff --git a/test/worker_test_stubs.cpp b/test/worker_test_stubs.cpp index 274a7d2..cd7764c 100644 --- a/test/worker_test_stubs.cpp +++ b/test/worker_test_stubs.cpp @@ -13,12 +13,12 @@ namespace { struct FakeSemaphore { - std::atomic available{false}; + std::atomic available{false}; }; struct FakeTask { - TaskFunction_t entry{nullptr}; - void *arg{nullptr}; + TaskFunction_t entry{nullptr}; + void *arg{nullptr}; }; std::atomic g_tickCount{0}; @@ -30,133 +30,135 @@ std::unordered_set g_liveTasks; TaskHandle_t g_currentTaskHandle = nullptr; -} // namespace +} // namespace extern "C" unsigned long millis(void) { - return static_cast(g_tickCount.load(std::memory_order_relaxed)); + return static_cast(g_tickCount.load(std::memory_order_relaxed)); } extern "C" SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t * /*buffer*/) { - auto *sem = new (std::nothrow) FakeSemaphore{}; - return reinterpret_cast(sem); + auto *sem = new (std::nothrow) FakeSemaphore{}; + return reinterpret_cast(sem); } extern "C" BaseType_t xSemaphoreTake(SemaphoreHandle_t handle, TickType_t /*ticks*/) { - if (!handle) { - return pdFALSE; - } - auto *sem = reinterpret_cast(handle); - bool expected = true; - if (sem->available.compare_exchange_strong(expected, false, std::memory_order_acq_rel)) { - return pdTRUE; - } - return pdFALSE; + if (!handle) { + return pdFALSE; + } + auto *sem = reinterpret_cast(handle); + bool expected = true; + if (sem->available.compare_exchange_strong(expected, false, std::memory_order_acq_rel)) { + return pdTRUE; + } + return pdFALSE; } extern "C" BaseType_t xSemaphoreGive(SemaphoreHandle_t handle) { - if (!handle) { - return pdFALSE; - } - auto *sem = reinterpret_cast(handle); - sem->available.store(true, std::memory_order_release); - return pdTRUE; + if (!handle) { + return pdFALSE; + } + auto *sem = reinterpret_cast(handle); + sem->available.store(true, std::memory_order_release); + return pdTRUE; } extern "C" void vSemaphoreDelete(SemaphoreHandle_t handle) { - auto *sem = reinterpret_cast(handle); - delete sem; -} - -extern "C" BaseType_t xTaskCreatePinnedToCore(TaskFunction_t task, - const char * /*name*/, - uint32_t /*stackDepth*/, - void *parameters, - UBaseType_t /*priority*/, - TaskHandle_t *createdTask, - BaseType_t /*coreId*/) { - auto *fakeTask = new (std::nothrow) FakeTask{task, parameters}; - if (!fakeTask) { - return pdFAIL; - } - - TaskHandle_t handle = reinterpret_cast(fakeTask); - if (createdTask) { - *createdTask = handle; - } - - { - std::lock_guard guard(g_taskMutex); - g_liveTasks.insert(handle); - } - - g_createdTasks.fetch_add(1, std::memory_order_relaxed); - return pdPASS; + auto *sem = reinterpret_cast(handle); + delete sem; +} + +extern "C" BaseType_t xTaskCreatePinnedToCore( + TaskFunction_t task, + const char * /*name*/, + uint32_t /*stackDepth*/, + void *parameters, + UBaseType_t /*priority*/, + TaskHandle_t *createdTask, + BaseType_t /*coreId*/ +) { + auto *fakeTask = new (std::nothrow) FakeTask{task, parameters}; + if (!fakeTask) { + return pdFAIL; + } + + TaskHandle_t handle = reinterpret_cast(fakeTask); + if (createdTask) { + *createdTask = handle; + } + + { + std::lock_guard guard(g_taskMutex); + g_liveTasks.insert(handle); + } + + g_createdTasks.fetch_add(1, std::memory_order_relaxed); + return pdPASS; } extern "C" void vTaskDelete(TaskHandle_t task) { - TaskHandle_t target = task ? task : g_currentTaskHandle; - if (!target) { - return; - } + TaskHandle_t target = task ? task : g_currentTaskHandle; + if (!target) { + return; + } - bool removed = false; - { - std::lock_guard guard(g_taskMutex); - removed = g_liveTasks.erase(target) > 0; - } + bool removed = false; + { + std::lock_guard guard(g_taskMutex); + removed = g_liveTasks.erase(target) > 0; + } - if (removed) { - g_deletedTasks.fetch_add(1, std::memory_order_relaxed); - auto *fakeTask = reinterpret_cast(target); - delete fakeTask; - } + if (removed) { + g_deletedTasks.fetch_add(1, std::memory_order_relaxed); + auto *fakeTask = reinterpret_cast(target); + delete fakeTask; + } } extern "C" void vTaskDelay(TickType_t ticks) { - g_tickCount.fetch_add(ticks, std::memory_order_relaxed); + g_tickCount.fetch_add(ticks, std::memory_order_relaxed); } extern "C" TickType_t xTaskGetTickCount(void) { - return g_tickCount.load(std::memory_order_relaxed); + return g_tickCount.load(std::memory_order_relaxed); } extern "C" TaskHandle_t xTaskGetCurrentTaskHandle(void) { - return g_currentTaskHandle; + return g_currentTaskHandle; } extern "C" void *heap_caps_malloc(size_t size, unsigned int /*caps*/) { - return std::malloc(size); + return std::malloc(size); } extern "C" void heap_caps_free(void *ptr) { - std::free(ptr); + std::free(ptr); } extern "C" size_t heap_caps_get_total_size(unsigned int /*caps*/) { - return 0; + return 0; } namespace test_support { void resetRuntime() { - g_tickCount.store(0, std::memory_order_relaxed); - g_createdTasks.store(0, std::memory_order_relaxed); - g_deletedTasks.store(0, std::memory_order_relaxed); + g_tickCount.store(0, std::memory_order_relaxed); + g_createdTasks.store(0, std::memory_order_relaxed); + g_deletedTasks.store(0, std::memory_order_relaxed); - std::lock_guard guard(g_taskMutex); - for (TaskHandle_t handle : g_liveTasks) { - auto *fakeTask = reinterpret_cast(handle); - delete fakeTask; - } - g_liveTasks.clear(); + std::lock_guard guard(g_taskMutex); + for (TaskHandle_t handle : g_liveTasks) { + auto *fakeTask = reinterpret_cast(handle); + delete fakeTask; + } + g_liveTasks.clear(); } 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