Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .claude/rules.zip
Binary file not shown.
1 change: 1 addition & 0 deletions .claude/rules/tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tests for projects should be placed in the `Tests/` or `tests/` subfolder of that project.
1 change: 0 additions & 1 deletion Documentation/ideas.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
- Make it possible to override stack size for an app via config file (loaded at boot), and make it possible to set preferred memory location (e.g. internal/external)
- Wrap file operations like fopen/fclose with file_mutex
- Add bold fonts for e-ink readability improvement
- Move test projects to their relevant subproject
- Httpd.cpp: warn if running on same CPU core (or task) as UI/LVGL/window manager.
- Improve Setup: Show "Step done" screen
- Improve Setup: Add keyboard/keypad navigation explanation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ project(AppModuleTests)
enable_language(C CXX ASM)


file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(AppModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})

target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ project(CryptModuleTests)

enable_language(C CXX ASM)

file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(CryptModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})

target_include_directories(CryptModuleTests PRIVATE ${DOCTESTINC})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cassert>
#include <cstdio>
#include <cstdlib>

#include "FreeRTOS.h"
#include "task.h"
Expand Down Expand Up @@ -43,7 +44,10 @@ int main(int argc, char** argv) {
1,
nullptr
);
assert(task_result == pdPASS);

if (task_result != pdPASS) {
return 1;
}

vTaskStartScheduler();

Expand All @@ -54,6 +58,7 @@ int main(int argc, char** argv) {
extern "C" {
// Required for FreeRTOS
void vAssertCalled(unsigned long line, const char* const file) {
__assert_fail("assert failed", file, line, "");
std::fprintf(stderr, "assert failed at %s:%lu\n", file, line);
std::abort();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ project(ServiceModuleTests)
enable_language(C CXX ASM)


file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(ServiceModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})

target_include_directories(ServiceModuleTests PRIVATE ${DOCTESTINC})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cassert>

#include "FreeRTOS.h"
#include "task.h"
Expand Down Expand Up @@ -43,7 +42,10 @@ int main(int argc, char** argv) {
1,
nullptr
);
assert(task_result == pdPASS);

if (task_result != pdPASS) {
return 1;
}

vTaskStartScheduler();

Expand Down
9 changes: 6 additions & 3 deletions Tactility/Source/file/File.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ bool deleteRecursively(const std::string& path) {
if (path.empty()) {
return true;
}
if (path == "/" || path == "." || path == "..") {
return true;
}

if (isDirectory(path)) {
std::vector<dirent> entries;
Expand All @@ -287,6 +290,9 @@ bool deleteRecursively(const std::string& path) {
}

for (const auto& entry : entries) {
if (strcmp(entry.d_name, ".") == 0 || strcmp(entry.d_name, "..") == 0) {
continue;
}
Comment thread
KenVanHoeylandt marked this conversation as resolved.
auto child_path = path + "/" + entry.d_name;
if (!deleteRecursively(child_path)) {
return false;
Expand All @@ -297,9 +303,6 @@ bool deleteRecursively(const std::string& path) {
} else if (isFile(path)) {
LOG_I(TAG, "Deleting %s", path.c_str());
return deleteFile(path);
} else if (path == "/" || path == "." || path == "..") {
// No-op
return true;
} else {
LOG_E(TAG, "Failed to delete \"%s\": unknown type", path.c_str());
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ project(TactilityTests)
enable_language(C CXX ASM)


file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/Source/*.cpp)
add_executable(TactilityTests EXCLUDE_FROM_ALL ${TEST_SOURCES})

target_include_directories(TactilityTests PRIVATE ${DOCTESTINC})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ using namespace tt;

TEST_CASE("findOrCreateDirectory can create a directory tree without prefix") {
CHECK_EQ(file::findOrCreateDirectory("test1/test1", 0777), true);
// TODO: delete dirs
CHECK_EQ(file::deleteRecursively("test1"), true);
}

TEST_CASE("findOrCreateDirectory can create a directory tree with prefix") {
CHECK_EQ(file::findOrCreateDirectory("/tmp/test2", 0777), true);
// TODO: delete dirs
CHECK_EQ(file::deleteRecursively("/tmp/test2"), true);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cassert>

#include "FreeRTOS.h"
#include "task.h"
Expand Down Expand Up @@ -54,7 +53,10 @@ int main(int argc, char** argv) {
1,
nullptr
);
assert(task_result == pdPASS);

if (task_result != pdPASS) {
return 1;
}

vTaskStartScheduler();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ TEST_CASE("Writing and reading multiple records to a file") {
CHECK_EQ(reader.open(), true);
CHECK_EQ(reader.hasNext(), true);
CHECK_EQ(reader.readNext(&record_in), true);
CHECK_EQ(record_in.value, 0xAAAAAAAA);
CHECK_EQ(reader.hasNext(), true);
CHECK_EQ(reader.readNext(&record_in), true);
CHECK_EQ(record_in.value, 0xBBBBBBBB);
CHECK_EQ(reader.hasNext(), false);
reader.close();

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ TEST_CASE("parseUrlQuery should url-decode the key") {
CHECK_EQ(map["Test!Test"], "value");
}

TEST_CASE("urlDecode") {
TEST_CASE("urlEncode") {
auto input = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\");
auto expected = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C");
auto encoded = network::urlEncode(input);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ project(TactilityFreeRtosTests)
enable_language(C CXX ASM)


file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/Source/*.cpp)
add_executable(TactilityFreeRtosTests EXCLUDE_FROM_ALL ${TEST_SOURCES})

target_include_directories(TactilityFreeRtosTests PRIVATE ${DOCTESTINC})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "doctest.h"
#include <Tactility/DispatcherThread.h>
#include <Tactility/Semaphore.h>

using namespace tt;

Expand All @@ -18,11 +19,14 @@ TEST_CASE("DispatcherThread should consume jobs") {
DispatcherThread thread("test");
thread.start();
int counter = 0;
Semaphore done(1, 0);

thread.dispatch([&counter]() { counter++; });

tt::kernel::delayTicks(10);
thread.dispatch([&counter, &done]() {
counter++;
done.release();
});

CHECK(done.acquire(pdMS_TO_TICKS(2000)));
CHECK_EQ(counter, 1);
thread.stop();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ TEST_CASE("a Mutex can block a thread") {
1024,
[&mutex] {
mutex.lock(kernel::FREERTOS_MAX_TICKS);
mutex.unlock();
return 0;
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ TEST_CASE("a RecursiveMutex can block a thread") {
1024,
[&mutex] {
mutex.lock(kernel::FREERTOS_MAX_TICKS);
mutex.unlock();
return 0;
}
);
Expand All @@ -35,4 +36,5 @@ TEST_CASE("a RecursiveMutex can be locked more than once from the same context")
CHECK_EQ(mutex.lock(0), true);
CHECK_EQ(mutex.lock(0), true);
mutex.unlock();
mutex.unlock();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "doctest.h"
#include <Tactility/Thread.h>

#include <atomic>

using namespace tt;

TEST_CASE("when a thread is started then its callback should be called") {
Expand All @@ -22,7 +24,7 @@ TEST_CASE("when a thread is started then its callback should be called") {
}

TEST_CASE("a thread can be started and stopped") {
bool interrupted = false;
std::atomic<bool> interrupted = false;
auto* thread = new Thread(
"interruptable thread",
4096,
Expand All @@ -42,7 +44,7 @@ TEST_CASE("a thread can be started and stopped") {
}

TEST_CASE("thread id should only be set at when thread is started") {
bool interrupted = false;
std::atomic<bool> interrupted = false;
auto* thread = new Thread(
"interruptable thread",
4096,
Expand All @@ -63,7 +65,7 @@ TEST_CASE("thread id should only be set at when thread is started") {
}

TEST_CASE("thread state should be correct") {
bool interrupted = false;
std::atomic<bool> interrupted = false;
auto* thread = new Thread(
"interruptable thread",
4096,
Expand Down
67 changes: 67 additions & 0 deletions TactilityFreeRtos/Tests/Source/TimerTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include "doctest.h"
#include <Tactility/Semaphore.h>
#include <Tactility/Timer.h>

#include <atomic>

using namespace tt;

// stop() only enqueues a command; it doesn't wait for the timer service task to process it or
// finish an in-flight callback. Queuing a pending callback on that same queue and waiting for it
// does, since the service task processes its queue in order.
void waitForTimerServiceIdle(Timer& timer) {
Semaphore done(1, 0);
auto markDone = [](void* context, uint32_t) {
static_cast<Semaphore*>(context)->release();
};
REQUIRE(timer.setPendingCallback(markDone, &done, 0, pdMS_TO_TICKS(2000)));
REQUIRE(done.acquire(pdMS_TO_TICKS(2000)));
Comment on lines +12 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 \
  'REQUIRE|DOCTEST_CONFIG_NO_EXCEPTIONS|TestFailureException' \
  TactilityFreeRtos/Tests TactilityFreeRtos Libraries 2>/dev/null || true

rg -n -C 6 \
  'waitForTimerServiceIdle|setPendingCallback|xTimerPendFunctionCall' \
  TactilityFreeRtos/Tests/Source/TimerTest.cpp \
  TactilityFreeRtos/Include/Tactility/Timer.h

Repository: TactilityProject/Tactility

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test helper and surrounding tests ---'
sed -n '1,240p' TactilityFreeRtos/Tests/Source/TimerTest.cpp

printf '%s\n' '--- timer callback API and implementation references ---'
rg -n -C 8 'setPendingCallback|xTimerPendFunctionCall|TimerCallback|Semaphore' \
  TactilityFreeRtos/Include TactilityFreeRtos/Source TactilityFreeRtos/Tests/Source \
  -g '*.h' -g '*.hpp' -g '*.cpp' -g '*.c' 2>/dev/null | head -n 1200

printf '%s\n' '--- doctest configuration and assertion behavior references ---'
rg -n -C 8 'DOCTEST_CONFIG_NO_EXCEPTIONS|TestFailureException|REQUIRE\(' \
  TactilityFreeRtos/Tests TactilityFreeRtos -g '*.h' -g '*.hpp' -g '*.cpp' -g '*.c' 2>/dev/null | head -n 1200

Repository: TactilityProject/Tactility

Length of output: 35940


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- doctest configuration ---'
git ls-files | rg -i '(^|/)(doctest|test|main|CMakeLists|meson|platformio|idf_component).*' | head -n 500
rg -n -C 5 'DOCTEST_CONFIG_NO_EXCEPTIONS|DOCTEST_CONFIG_IMPLEMENT|TestFailureException|TEST_CASE|REQUIRE\(' \
  --glob '!Libraries/SDL/**' --glob '!Libraries/lvgl/**' --glob '!Libraries/mbedtls/**' \
  . 2>/dev/null | head -n 1600

printf '%s\n' '--- exact Timer API and callback declarations ---'
sed -n '1,155p' TactilityFreeRtos/Include/Tactility/Timer.h

printf '%s\n' '--- timer service and pending-call implementation references ---'
rg -n -C 12 'xTimerPendFunctionCall|timer service|TimerPendedFunction|pend.*function|Pended' \
  TactilityFreeRtos Libraries -g '*.c' -g '*.cpp' -g '*.h' -g '*.hpp' 2>/dev/null | head -n 1600

Repository: TactilityProject/Tactility

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- doctest files and configuration ---'
git ls-files | rg -i 'doctest|test[_-]?main|main[_-]?test|tests?/.*main' | head -n 300
rg -n 'DOCTEST_CONFIG_NO_EXCEPTIONS|TestFailureException' \
  TactilityFreeRtos TactilityKernel cmake CMakeLists.txt 2>/dev/null | head -n 300 || true

printf '%s\n' '--- Timer.h relevant declarations ---'
nl -ba TactilityFreeRtos/Include/Tactility/Timer.h | sed -n '20,145p'

printf '%s\n' '--- pending callback implementations in project files ---'
rg -n 'xTimerPendFunctionCall|setPendingCallback' \
  TactilityFreeRtos TactilityKernel -g '*.c' -g '*.cpp' -g '*.h' -g '*.hpp' 2>/dev/null | head -n 300

printf '%s\n' '--- test target definitions ---'
rg -n -C 6 'TimerTest.cpp|doctest|DOCTEST' \
  TactilityFreeRtos -g 'CMakeLists.txt' -g '*.cmake' -g '*.cpp' -g '*.h' 2>/dev/null | head -n 600

Repository: TactilityProject/Tactility

Length of output: 635


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- doctest configuration ---'
rg -n -C 8 'DOCTEST_CONFIG_NO_EXCEPTIONS|TestFailureException|DOCTEST_CONFIG_IMPLEMENT' \
  TactilityFreeRtos/Tests/Source/Main.cpp \
  TactilityFreeRtos/Tests \
  Tests/Doctest/Include/doctest.h 2>/dev/null | head -n 500 || true

printf '%s\n' '--- Timer.h relevant declarations ---'
cat -n TactilityFreeRtos/Include/Tactility/Timer.h | sed -n '20,145p'

printf '%s\n' '--- pending callback implementations in project files ---'
rg -n -C 10 'xTimerPendFunctionCall|setPendingCallback' \
  TactilityFreeRtos TactilityKernel -g '*.c' -g '*.cpp' -g '*.h' -g '*.hpp' 2>/dev/null | head -n 500

printf '%s\n' '--- test target definitions ---'
rg -n -C 6 'TimerTest.cpp|doctest|DOCTEST' \
  TactilityFreeRtos -g 'CMakeLists.txt' -g '*.cmake' -g '*.cpp' -g '*.h' 2>/dev/null | head -n 500

Repository: TactilityProject/Tactility

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

timer_test = Path("TactilityFreeRtos/Tests/Source/TimerTest.cpp").read_text()
timer_h = Path("TactilityFreeRtos/Include/Tactility/Timer.h").read_text()
semaphore_h = Path("TactilityFreeRtos/Include/Tactility/Semaphore.h").read_text()
doctest_h = Path("Tests/Doctest/Include/doctest.h").read_text()

checks = {
    "automatic completion context": re.search(
        r"void waitForTimerServiceIdle\(Timer& timer\)\s*\{.*?Semaphore done\(1,\s*0\);",
        timer_test, re.S,
    ),
    "stack context passed to asynchronous callback": re.search(
        r"setPendingCallback\(markDone,\s*&done,", timer_test,
    ),
    "callback dereferences context": re.search(
        r"static_cast<Semaphore\*>(context\)->release\(\);", timer_test,
    ),
    "pending callback uses timer service queue": re.search(
        r"xTimerPendFunctionCall\(newCallback,\s*callbackContext,\s*callbackArg,\s*timeout\)",
        timer_h,
    ),
    "Semaphore owns a deletable handle": re.search(
        r"std::unique_ptr<.*SemaphoreHandleDeleter>\s+handle;", semaphore_h,
    ),
    "Semaphore destructor is present": re.search(
        r"~Semaphore\(\)\s+override\s*\{", semaphore_h,
    ),
    "doctest throws on failed assertion when exceptions are enabled": re.search(
        r"void throwException\(\)\s*\{\s*g_cs->shouldLogCurrentException\s*=\s*false;\s*throw TestFailureException\(\);",
        doctest_h, re.S,
    ),
    "doctest catches the failure outside the test body": re.search(
        r"catch\(const TestFailureException&\)\s*\{",
        doctest_h,
    ),
}

for name, match in checks.items():
    print(f"{name}: {'present' if match else 'missing'}")

if not all(checks.values()):
    raise SystemExit("source invariant check failed")

print("Conclusion: a failed second REQUIRE can unwind after enqueueing the callback, while the callback still holds &done.")
PY

Repository: TactilityProject/Tactility

Length of output: 988


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

timer_test = Path("TactilityFreeRtos/Tests/Source/TimerTest.cpp").read_text()
timer_h = Path("TactilityFreeRtos/Include/Tactility/Timer.h").read_text()
semaphore_h = Path("TactilityFreeRtos/Include/Tactility/Semaphore.h").read_text()
doctest_h = Path("Tests/Doctest/Include/doctest.h").read_text()

checks = {
    "automatic completion context":
        "Semaphore done(1, 0);" in timer_test,
    "stack context passed to asynchronous callback":
        "setPendingCallback(markDone, &done," in timer_test,
    "callback dereferences context":
        "static_cast<Semaphore*>(context)->release();" in timer_test,
    "pending callback uses timer service queue":
        "xTimerPendFunctionCall(newCallback, callbackContext, callbackArg, timeout)" in timer_h,
    "Semaphore owns a deletable handle":
        "SemaphoreHandleDeleter> handle;" in semaphore_h,
    "Semaphore destructor is present":
        "~Semaphore() override" in semaphore_h,
    "doctest throws on failed assertion":
        "throw TestFailureException();" in doctest_h,
    "doctest catches the failure outside the test body":
        "catch(const TestFailureException&)" in doctest_h,
}

for name, present in checks.items():
    print(f"{name}: {'present' if present else 'missing'}")

if not all(checks.values()):
    raise SystemExit("source invariant check failed")

print("Conclusion: the helper passes a stack Semaphore to a timer-service callback, and a failed REQUIRE can unwind before that callback finishes.")
PY

Repository: TactilityProject/Tactility

Length of output: 674


Keep the completion context alive until markDone returns.

If done.acquire(...) times out, REQUIRE unwinds the helper while the timer-service callback can still execute. markDone then dereferences the destroyed stack Semaphore, whose owned FreeRTOS handle has also been deleted. Use a completion context that remains valid through the callback, including the timeout path.

Source: MCP tools

}

TEST_CASE("TimerType::Periodic timers can be stopped and restarted") {
std::atomic<int> counter{0};
auto* timer = new Timer(Timer::Type::Periodic, 1, [&counter] { counter++; });
CHECK_EQ(timer->start(), true);
kernel::delayTicks(10);
CHECK_EQ(timer->stop(), true);
waitForTimerServiceIdle(*timer);
const auto first_run_count = counter.load();

CHECK_EQ(timer->start(), true);
kernel::delayTicks(10);
Comment on lines +28 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that the first periodic run produced a callback.

first_run_count is only used as a baseline. If the first run produces zero callbacks and the second run produces one, CHECK_GT(counter.load(), first_run_count) passes. The test then does not prove that the timer worked before restart.

Add a positive assertion before the second start():

🧪 Proposed assertion
     const auto first_run_count = counter.load();
+    CHECK_GT(first_run_count, 0);
 
     CHECK_EQ(timer->start(), true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const auto first_run_count = counter.load();
CHECK_EQ(timer->start(), true);
kernel::delayTicks(10);
const auto first_run_count = counter.load();
CHECK_GT(first_run_count, 0);
CHECK_EQ(timer->start(), true);
kernel::delayTicks(10);

CHECK_EQ(timer->stop(), true);
waitForTimerServiceIdle(*timer);
delete timer;

CHECK_GT(counter.load(), first_run_count);
}

TEST_CASE("TimerType::Periodic calls the callback periodically") {
int ticks_to_run = 10;
std::atomic<int> counter{0};
auto* timer = new Timer(Timer::Type::Periodic, 1, [&counter] { counter++; });
CHECK_EQ(timer->start(), true);
kernel::delayTicks(ticks_to_run);
CHECK_EQ(timer->stop(), true);
waitForTimerServiceIdle(*timer);
delete timer;

// Exact count isn't guaranteed (scheduling slop around start()/stop()), so this only checks
// that the callback fired repeatedly, not an exact tick-for-tick match.
CHECK_GE(counter.load(), ticks_to_run / 2);
}

TEST_CASE("restarting TimerType::Once timers calls the callback again") {
std::atomic<int> counter{0};
auto* timer = new Timer(Timer::Type::Once, 1, [&counter] { counter++; });
CHECK_EQ(timer->start(), true);
kernel::delayTicks(10);
CHECK_EQ(timer->stop(), true);
CHECK_EQ(timer->start(), true);
kernel::delayTicks(10);
CHECK_EQ(timer->stop(), true);
waitForTimerServiceIdle(*timer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'restarting TimerType::Once|timer->stop\(\)|timer->start\(\)|waitForTimerServiceIdle' \
  TactilityFreeRtos/Tests/Source/TimerTest.cpp \
  TactilityFreeRtos/Include/Tactility/Timer.h

Repository: TactilityProject/Tactility

Length of output: 4943


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- TimerTest.cpp ---'
cat -n TactilityFreeRtos/Tests/Source/TimerTest.cpp | sed -n '1,90p'

printf '%s\n' '--- Timer.h ---'
cat -n TactilityFreeRtos/Include/Tactility/Timer.h | sed -n '1,220p'

printf '%s\n' '--- timer implementation and test references ---'
rg -n -C 5 'Timer::(start|stop)|xTimer(Start|Stop)|timer service|waitForTimerServiceIdle' \
  TactilityFreeRtos TactilityFreeRtos/Tests

Repository: TactilityProject/Tactility

Length of output: 19063


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- timer API implementations and compatibility layers ---'
rg -n -C 6 \
  'xTimerStop|xTimerStart|xTimerPendFunctionCall|TimerService|timer command|tmrCOMMAND_STOP|tmrCOMMAND_START' \
  . -g '!build' -g '!dist' -g '!node_modules' | sed -n '1,260p'

printf '%s\n' '--- relevant repository files ---'
git ls-files | rg '(^|/)(Timer|Timers|FreeRTOS|freertos|TimerTest)' | sed -n '1,160p'

printf '%s\n' '--- deterministic queue-order model ---'
python3 - <<'PY'
from collections import deque

def process(commands, expiry_pending):
    queue = deque(commands)
    callbacks = 0
    active = True
    while queue:
        command = queue.popleft()
        if command == "expiry":
            if active:
                callbacks += 1
                active = False
        elif command == "stop":
            active = False
        elif command == "start":
            active = True
        elif command == "marker":
            return callbacks, active, list(queue)
    return callbacks, active, []

for first_expiry in (False, True):
    without_wait = ["stop", "start", "expiry" if first_expiry else "marker", "stop", "marker"]
    with_wait = ["stop", "marker", "start", "expiry" if first_expiry else "marker", "stop", "marker"]
    print({
        "expiry_already_queued": first_expiry,
        "without_wait": process(without_wait, first_expiry),
        "with_wait": process(with_wait, first_expiry),
    })
PY

Repository: TactilityProject/Tactility

Length of output: 24876


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- FreeRTOS timer command implementation ---'
fd -i 'timers.c' Libraries TactilityFreeRtos TactilityKernel Platforms Devices
fd -i '*timer*.c' Libraries/FreeRTOS-Kernel | sed -n '1,80p'

printf '%s\n' '--- timer command and callback processing ---'
rg -n -C 8 \
  'xTimerGenericCommand|prvProcessReceivedCommands|prvProcessExpiredTimer|tmrCOMMAND_EXECUTE_CALLBACK|xTimerPendFunctionCall' \
  Libraries/FreeRTOS-Kernel -g '*.c' -g '*.h' | sed -n '1,360p'

Repository: TactilityProject/Tactility

Length of output: 534


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- timer command and callback processing ---'
rg -n -C 10 \
  'xTimerGenericCommand|prvProcessReceivedCommands|prvProcessExpiredTimer|tmrCOMMAND_EXECUTE_CALLBACK|xTimerPendFunctionCall' \
  Libraries/FreeRTOS-Kernel/timers.c Libraries/FreeRTOS-Kernel/include/timers.h | sed -n '1,420p'

Repository: TactilityProject/Tactility

Length of output: 34799


Synchronize the first one-shot run before restarting.

Timer::stop() only queues a command and does not wait for timer-service processing. Add waitForTimerServiceIdle(*timer) after the first stop() and before the second start(). Otherwise, the first callback can be suppressed, making the exact-two assertion scheduling-dependent.

Source: MCP tools

delete timer;

CHECK_EQ(counter.load(), 2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ project(TactilityKernelTests)
enable_language(C CXX ASM)


file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(TactilityKernelTests EXCLUDE_FROM_ALL ${TEST_SOURCES})

target_include_directories(TactilityKernelTests PRIVATE ${DOCTESTINC})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cassert>
#include <tactility/check.h>

#include <tactility/dts.h>
Expand Down Expand Up @@ -52,7 +51,10 @@ int main(int argc, char** argv) {
1,
nullptr
);
assert(task_result == pdPASS);

if (task_result != pdPASS) {
return 1;
}

vTaskStartScheduler();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ TEST_CASE("Global symbol resolution") {
REQUIRE_EQ(module_add(&module), ERROR_NONE);
CHECK_EQ(module_resolve_symbol_global("symbol_test_function", &addr), false);
REQUIRE_EQ(module_start(&module), ERROR_NONE);
// Still fails as symbols are null
// Resolvable now that the module is both added and started
CHECK_EQ(module_resolve_symbol_global("symbol_test_function", &addr), true);
// Cleanup
CHECK_EQ(module_stop(&module), ERROR_NONE);
CHECK_EQ(module_remove(&module), ERROR_NONE);

CHECK_EQ(module_destruct(&module), ERROR_NONE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ TEST_CASE("mutex_lock in another task should block when a lock is active") {
Mutex* mutex_ptr = static_cast<Mutex*>(input);
mutex_lock(mutex_ptr);
task_lock_counter++;
mutex_unlock(mutex_ptr);
vTaskDelete(nullptr);
},
"mutex_test",
Expand Down
Loading
Loading