Kernel improvements (#485)

* **New Features**
  * Added public accessors for querying module/device start and ready state.

* **Refactor**
  * Internal state moved to opaque internal objects; module/device/driver initializers now explicitly initialize internal pointers.
  * Lifecycle handling updated to construct/destruct internal state and use accessors.

* **Tests**
  * Tests updated to use public accessors and explicit construct/destruct lifecycle calls.

* **Chores**
  * Test build/include paths and small metadata updated.
This commit is contained in:
Ken Van Hoeylandt
2026-02-06 16:32:30 +01:00
committed by GitHub
parent 1757af859c
commit 79e43b093a
105 changed files with 338 additions and 331 deletions
+223
View File
@@ -0,0 +1,223 @@
#include "doctest.h"
#include <cstring>
#include <vector>
#include <tactility/device.h>
#include <tactility/module.h>
static Module module = {
.name = "test_module",
.start = nullptr,
.stop = nullptr
};
TEST_CASE("device_construct and device_destruct should set and unset the internal field") {
Device device = { 0 };
error_t error = device_construct(&device);
CHECK_EQ(error, ERROR_NONE);
CHECK_NE(device.internal, nullptr);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
CHECK_EQ(device.internal, nullptr);
}
TEST_CASE("device_add should add the device to the list of all devices") {
Device device = {
.name = "device",
.config = nullptr,
.parent = nullptr,
.internal = nullptr
};
CHECK_EQ(device_construct(&device), ERROR_NONE);
CHECK_EQ(device_add(&device), ERROR_NONE);
// Gather all devices
std::vector<Device*> devices;
device_for_each(&devices, [](auto* device, auto* context) {
auto* devices_ptr = static_cast<std::vector<Device*>*>(context);
devices_ptr->push_back(device);
return true;
});
CHECK_EQ(devices.size(), 1);
CHECK_EQ(devices[0], &device);
CHECK_EQ(device_remove(&device), ERROR_NONE);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
}
TEST_CASE("device_add should add the device to its parent") {
Device parent = {
.name = "parent",
.config = nullptr,
.parent = nullptr,
.internal = nullptr
};
Device child = {
.name = "child",
.config = nullptr,
.parent = &parent,
.internal = nullptr
};
CHECK_EQ(device_construct(&parent), ERROR_NONE);
CHECK_EQ(device_add(&parent), ERROR_NONE);
CHECK_EQ(device_construct(&child), ERROR_NONE);
CHECK_EQ(device_add(&child), ERROR_NONE);
// Gather all child devices
std::vector<Device*> children;
device_for_each_child(&parent, &children, [](auto* child_device, auto* context) {
auto* children_ptr = (std::vector<Device*>*)context;
children_ptr->push_back(child_device);
return true;
});
CHECK_EQ(children.size(), 1);
CHECK_EQ(children[0], &child);
CHECK_EQ(device_remove(&child), ERROR_NONE);
CHECK_EQ(device_destruct(&child), ERROR_NONE);
CHECK_EQ(device_remove(&parent), ERROR_NONE);
CHECK_EQ(device_destruct(&parent), ERROR_NONE);
}
TEST_CASE("device_add should set the state to 'added'") {
Device device = {
.name = "device",
.config = nullptr,
.parent = nullptr,
.internal = nullptr
};
CHECK_EQ(device_construct(&device), ERROR_NONE);
CHECK_EQ(device_is_added(&device), false);
CHECK_EQ(device_add(&device), ERROR_NONE);
CHECK_EQ(device_is_added(&device), true);
CHECK_EQ(device_remove(&device), ERROR_NONE);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
}
TEST_CASE("device_remove should remove it from the list of all devices") {
Device device = {
.name = "device",
.config = nullptr,
.parent = nullptr,
.internal = nullptr
};
CHECK_EQ(device_construct(&device), ERROR_NONE);
CHECK_EQ(device_add(&device), ERROR_NONE);
CHECK_EQ(device_remove(&device), ERROR_NONE);
// Gather all devices
std::vector<Device*> devices;
device_for_each(&devices, [](auto* device, auto* context) {
auto* devices_ptr = (std::vector<Device*>*)context;
devices_ptr->push_back(device);
return true;
});
CHECK_EQ(devices.size(), 0);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
}
TEST_CASE("device_remove should remove the device from its parent") {
Device parent = {
.name = "parent",
.config = nullptr,
.parent = nullptr,
.internal = nullptr
};
Device child = {
.name = "child",
.config = nullptr,
.parent = &parent,
.internal = nullptr
};
CHECK_EQ(device_construct(&parent), ERROR_NONE);
CHECK_EQ(device_add(&parent), ERROR_NONE);
CHECK_EQ(device_construct(&child), ERROR_NONE);
CHECK_EQ(device_add(&child), ERROR_NONE);
CHECK_EQ(device_remove(&child), ERROR_NONE);
// Gather all child devices
std::vector<Device*> children;
device_for_each_child(&parent, &children, [](auto* child_device, auto* context) {
auto* children_ptr = (std::vector<Device*>*)context;
children_ptr->push_back(child_device);
return true;
});
CHECK_EQ(children.size(), 0);
CHECK_EQ(device_destruct(&child), ERROR_NONE);
CHECK_EQ(device_remove(&parent), ERROR_NONE);
CHECK_EQ(device_destruct(&parent), ERROR_NONE);
}
TEST_CASE("device_remove should clear the state 'added'") {
Device device = {
.name = "device",
.config = nullptr,
.parent = nullptr,
.internal = nullptr
};
CHECK_EQ(device_construct(&device), ERROR_NONE);
CHECK_EQ(device_add(&device), ERROR_NONE);
CHECK_EQ(device_is_added(&device), true);
CHECK_EQ(device_remove(&device), ERROR_NONE);
CHECK_EQ(device_is_added(&device), false);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
}
TEST_CASE("device_is_ready should return true only when it is started") {
const char* compatible[] = { "test_compatible", nullptr };
Driver driver = {
.name = "test_driver",
.compatible = compatible,
.start_device = nullptr,
.stop_device = nullptr,
.api = nullptr,
.device_type = nullptr,
.owner = &module,
.internal = nullptr
};
Device device = { 0 };
CHECK_EQ(driver_construct_add(&driver), ERROR_NONE);
CHECK_EQ(device_construct(&device), ERROR_NONE);
CHECK_EQ(device_is_ready(&device), false);
device_set_driver(&device, &driver);
CHECK_EQ(device_is_ready(&device), false);
CHECK_EQ(device_add(&device), ERROR_NONE);
CHECK_EQ(device_is_ready(&device), false);
CHECK_EQ(device_start(&device), ERROR_NONE);
CHECK_EQ(device_is_ready(&device), true);
CHECK_EQ(device_stop(&device), ERROR_NONE);
CHECK_EQ(device_is_ready(&device), false);
CHECK_EQ(device_remove(&device), ERROR_NONE);
CHECK_EQ(device_is_ready(&device), false);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
CHECK_EQ(driver_remove_destruct(&driver), ERROR_NONE);
}
@@ -0,0 +1,25 @@
#include "doctest.h"
#include <tactility/freertos/task.h>
#include <tactility/concurrent/dispatcher.h>
TEST_CASE("dispatcher test") {
DispatcherHandle_t dispatcher = dispatcher_alloc();
CHECK_NE(dispatcher, nullptr);
int count = 0;
auto error = dispatcher_dispatch(dispatcher, &count, [](void* context) {
int* count_ptr = static_cast<int*>(context);
(*count_ptr)++;
});
CHECK_EQ(error, ERROR_NONE);
vTaskDelay(1);
CHECK_EQ(count, 0);
CHECK_EQ(dispatcher_consume(dispatcher), ERROR_NONE);
CHECK_EQ(count, 1);
dispatcher_free(dispatcher);
}
@@ -0,0 +1,72 @@
#include "doctest.h"
#include <tactility/driver.h>
#include <tactility/device.h>
#include <tactility/module.h>
static Module module = {
.name = "test_module",
.start = nullptr,
.stop = nullptr
};
struct IntegrationDriverConfig {
int startResult;
int stopResult;
};
static int startCalled = 0;
static int stopCalled = 0;
#define integration_data(device) static_cast<IntegrationDriverData*>(device_get_driver_data(device))
#define integration_config(device) static_cast<const IntegrationDriverConfig*>(device->config)
static int start(Device* device) {
startCalled++;
return integration_config(device)->startResult;
}
static int stop(Device* device) {
stopCalled++;
return integration_config(device)->stopResult;
}
static Driver integration_driver = {
.name = "integration_test_driver",
.compatible = (const char*[]) { "integration", nullptr },
.start_device = start,
.stop_device = stop,
.api = nullptr,
.device_type = nullptr,
.owner = &module,
.internal = nullptr,
};
TEST_CASE("driver with with start success and stop success should start and stop a device") {
startCalled = 0;
stopCalled = 0;
static const IntegrationDriverConfig config {
.startResult = 0,
.stopResult = 0
};
static Device integration_device {
.name = "integration_device",
.config = &config,
.parent = nullptr,
};
CHECK_EQ(driver_construct_add(&integration_driver), ERROR_NONE);
CHECK_EQ(device_construct(&integration_device), ERROR_NONE);
device_add(&integration_device);
CHECK_EQ(startCalled, 0);
CHECK_EQ(driver_bind(&integration_driver, &integration_device), ERROR_NONE);
CHECK_EQ(startCalled, 1);
CHECK_EQ(stopCalled, 0);
CHECK_EQ(driver_unbind(&integration_driver, &integration_device), ERROR_NONE);
CHECK_EQ(stopCalled, 1);
CHECK_EQ(device_remove(&integration_device), ERROR_NONE);
CHECK_EQ(device_destruct(&integration_device), ERROR_NONE);
CHECK_EQ(driver_remove_destruct(&integration_driver), ERROR_NONE);
}
@@ -0,0 +1,77 @@
#include "doctest.h"
#include <tactility/driver.h>
#include <tactility/module.h>
static Module module = {
.name = "test_module",
.start = nullptr,
.stop = nullptr
};
TEST_CASE("driver_construct and driver_destruct should set and unset the correct fields") {
Driver driver = { 0 };
driver.owner = &module;
CHECK_EQ(driver_construct(&driver), ERROR_NONE);
CHECK_EQ(driver_add(&driver), ERROR_NONE);
CHECK_NE(driver.internal, nullptr);
CHECK_EQ(driver_remove(&driver), ERROR_NONE);
CHECK_EQ(driver_destruct(&driver), ERROR_NONE);
CHECK_EQ(driver.internal, nullptr);
}
TEST_CASE("a driver without a module should not be destructible") {
Driver driver = { 0 };
CHECK_EQ(driver_construct(&driver), ERROR_NONE);
CHECK_EQ(driver_destruct(&driver), ERROR_NOT_ALLOWED);
driver.owner = &module;
CHECK_EQ(driver_destruct(&driver), ERROR_NONE);
}
TEST_CASE("driver_is_compatible should return true if a compatible value is found") {
const char* compatible[] = { "test_compatible", nullptr };
Driver driver = {
.name = "test_driver",
.compatible = compatible,
.start_device = nullptr,
.stop_device = nullptr,
.api = nullptr,
.device_type = nullptr,
.owner = &module,
.internal = nullptr
};
CHECK_EQ(driver_is_compatible(&driver, "test_compatible"), true);
CHECK_EQ(driver_is_compatible(&driver, "nope"), false);
CHECK_EQ(driver_is_compatible(&driver, nullptr), false);
}
TEST_CASE("driver_find should only find a compatible driver when the driver was constructed") {
const char* compatible[] = { "test_compatible", nullptr };
Driver driver = {
.name = "test_driver",
.compatible = compatible,
.start_device = nullptr,
.stop_device = nullptr,
.api = nullptr,
.device_type = nullptr,
.owner = &module,
.internal = nullptr
};
Driver* found_driver = driver_find_compatible("test_compatible");
CHECK_EQ(found_driver, nullptr);
CHECK_EQ(driver_construct(&driver), ERROR_NONE);
CHECK_EQ(driver_add(&driver), ERROR_NONE);
found_driver = driver_find_compatible("test_compatible");
CHECK_EQ(found_driver, &driver);
CHECK_EQ(driver_remove(&driver), ERROR_NONE);
CHECK_EQ(driver_destruct(&driver), ERROR_NONE);
found_driver = driver_find_compatible("test_compatible");
CHECK_EQ(found_driver, nullptr);
}
+59
View File
@@ -0,0 +1,59 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cassert>
#include <tactility/freertos/task.h>
#include <tactility/kernel_init.h>
typedef struct {
int argc;
char** argv;
int result;
} TestTaskData;
extern "C" {
// From the relevant platform
extern struct Module platform_module;
}
void test_task(void* parameter) {
auto* data = (TestTaskData*)parameter;
doctest::Context context;
context.applyCommandLine(data->argc, data->argv);
// overrides
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
check(kernel_init(&platform_module, nullptr, nullptr) == ERROR_NONE);
data->result = context.run();
vTaskEndScheduler();
vTaskDelete(nullptr);
}
int main(int argc, char** argv) {
TestTaskData data = {
.argc = argc,
.argv = argv,
.result = 0
};
BaseType_t task_result = xTaskCreate(
test_task,
"test_task",
8192,
&data,
1,
nullptr
);
assert(task_result == pdPASS);
vTaskStartScheduler();
return data.result;
}
+141
View File
@@ -0,0 +1,141 @@
#include "doctest.h"
#include <tactility/module.h>
static void symbol_test_function() { /* NO-OP */ }
static error_t test_start_result = ERROR_NONE;
static bool start_called = false;
static error_t test_start() {
start_called = true;
return test_start_result;
}
static error_t test_stop_result = ERROR_NONE;
static bool stop_called = false;
static error_t test_stop() {
stop_called = true;
return test_stop_result;
}
TEST_CASE("Module construction and destruction") {
struct Module module = {
.name = "test",
.start = test_start,
.stop = test_stop,
.symbols = nullptr,
.internal = nullptr
};
// Test successful construction
CHECK_EQ(module_construct(&module), ERROR_NONE);
CHECK_EQ(module_is_started(&module), false);
// Test successful destruction
CHECK_EQ(module_destruct(&module), ERROR_NONE);
}
TEST_CASE("Module registration") {
struct Module module = {
.name = "test",
.start = test_start,
.stop = test_stop,
.symbols = nullptr,
.internal = nullptr
};
// module_add should succeed
CHECK_EQ(module_add(&module), ERROR_NONE);
// module_remove should succeed
CHECK_EQ(module_remove(&module), ERROR_NONE);
}
TEST_CASE("Module lifecycle") {
start_called = false;
stop_called = false;
test_start_result = ERROR_NONE;
test_stop_result = ERROR_NONE;
struct Module module = {
.name = "test",
.start = test_start,
.stop = test_stop,
.symbols = nullptr,
.internal = nullptr
};
CHECK_EQ(module_construct(&module), ERROR_NONE);
// 1. Successful start (no parent required anymore)
CHECK_EQ(module_start(&module), ERROR_NONE);
CHECK_EQ(module_is_started(&module), true);
CHECK_EQ(start_called, true);
// Start when already started (should return ERROR_NONE)
start_called = false;
CHECK_EQ(module_start(&module), ERROR_NONE);
CHECK_EQ(start_called, false); // start() function should NOT be called again
// Stop successful
CHECK_EQ(module_stop(&module), ERROR_NONE);
CHECK_EQ(module_is_started(&module), false);
CHECK_EQ(stop_called, true);
// Stop when already stopped (should return ERROR_NONE)
stop_called = false;
CHECK_EQ(module_stop(&module), ERROR_NONE);
CHECK_EQ(stop_called, false); // stop() function should NOT be called again
// Test failed start
test_start_result = ERROR_NOT_FOUND;
start_called = false;
CHECK_EQ(module_start(&module), ERROR_NOT_FOUND);
CHECK_EQ(module_is_started(&module), false);
CHECK_EQ(start_called, true);
// Test failed stop
test_start_result = ERROR_NONE;
CHECK_EQ(module_start(&module), ERROR_NONE);
test_stop_result = ERROR_NOT_SUPPORTED;
stop_called = false;
CHECK_EQ(module_stop(&module), ERROR_NOT_SUPPORTED);
CHECK_EQ(module_is_started(&module), true); // Should still be started if stop failed
CHECK_EQ(stop_called, true);
// Clean up: fix stop result so we can stop it
test_stop_result = ERROR_NONE;
CHECK_EQ(module_stop(&module), ERROR_NONE);
CHECK_EQ(module_destruct(&module), ERROR_NONE);
}
TEST_CASE("Global symbol resolution") {
static const struct ModuleSymbol test_symbols[] = {
DEFINE_MODULE_SYMBOL(symbol_test_function),
MODULE_SYMBOL_TERMINATOR
};
struct Module module = {
.name = "test_sym",
.start = test_start,
.stop = test_stop,
.symbols = test_symbols,
.internal = nullptr
};
REQUIRE_EQ(module_construct(&module), ERROR_NONE);
uintptr_t addr;
// Should fail as it is not added or started
CHECK_EQ(module_resolve_symbol_global("symbol_test_function", &addr), false);
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
CHECK_EQ(module_resolve_symbol_global("symbol_test_function", &addr), true);
// Cleanup
CHECK_EQ(module_remove(&module), ERROR_NONE);
CHECK_EQ(module_destruct(&module), ERROR_NONE);
}
@@ -0,0 +1,66 @@
#include "doctest.h"
#include <tactility/concurrent/mutex.h>
TEST_CASE("mutex_construct and mutex_destruct should properly set the handle") {
Mutex mutex = { 0 };
mutex_construct(&mutex);
CHECK_NE(mutex.handle, nullptr);
mutex_destruct(&mutex);
CHECK_EQ(mutex.handle, nullptr);
}
TEST_CASE("mutex_is_locked should return true only when the mutex is locked") {
Mutex mutex = { 0 };
mutex_construct(&mutex);
CHECK_EQ(mutex_is_locked(&mutex), false);
mutex_lock(&mutex);
CHECK_EQ(mutex_is_locked(&mutex), true);
mutex_unlock(&mutex);
CHECK_EQ(mutex_is_locked(&mutex), false);
mutex_destruct(&mutex);
}
TEST_CASE("mutex_try_lock should succeed on first lock but not on second") {
Mutex mutex = { 0 };
mutex_construct(&mutex);
CHECK_EQ(mutex_try_lock(&mutex), true);
CHECK_EQ(mutex_try_lock(&mutex), false);
mutex_unlock(&mutex);
mutex_destruct(&mutex);
}
TEST_CASE("mutex_lock in another task should block when a lock is active") {
static int task_lock_counter = 0;
Mutex mutex = { 0 };
task_lock_counter = 0;
mutex_construct(&mutex);
mutex_lock(&mutex);
TaskHandle_t task_handle;
auto task_create_result = xTaskCreate(
[](void* input) {
Mutex* mutex_ptr = static_cast<Mutex*>(input);
mutex_lock(mutex_ptr);
task_lock_counter++;
vTaskDelete(nullptr);
},
"mutex_test",
2048,
&mutex,
0,
&task_handle
);
CHECK_EQ(task_create_result, pdPASS);
CHECK_EQ(task_lock_counter, 0);
mutex_unlock(&mutex);
vTaskDelay(2); // 1 is sufficient most of the time, but not always
CHECK_EQ(task_lock_counter, 1);
mutex_destruct(&mutex);
}
@@ -0,0 +1,86 @@
#include "doctest.h"
#include <tactility/concurrent/recursive_mutex.h>
TEST_CASE("recursive_mutex_construct and mutex_destruct should properly set the handle") {
RecursiveMutex mutex = { 0 };
recursive_mutex_construct(&mutex);
CHECK_NE(mutex.handle, nullptr);
recursive_mutex_destruct(&mutex);
CHECK_EQ(mutex.handle, nullptr);
}
TEST_CASE("recursive_mutex_is_locked should return true only when the mutex is locked") {
RecursiveMutex mutex = { 0 };
recursive_mutex_construct(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), false);
recursive_mutex_lock(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), true);
recursive_mutex_unlock(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), false);
recursive_mutex_destruct(&mutex);
}
TEST_CASE("recursive_mutex_is_locked can lock twice from the same thread") {
RecursiveMutex mutex = { 0 };
recursive_mutex_construct(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), false);
recursive_mutex_lock(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), true);
recursive_mutex_lock(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), true);
recursive_mutex_unlock(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), true);
recursive_mutex_unlock(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), false);
recursive_mutex_destruct(&mutex);
}
TEST_CASE("recursive_mutex_try_lock should lock multiple times from the same thread") {
RecursiveMutex mutex = { 0 };
recursive_mutex_construct(&mutex);
CHECK_EQ(recursive_mutex_try_lock(&mutex), true);
CHECK_EQ(recursive_mutex_try_lock(&mutex), true);
recursive_mutex_unlock(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), true);
recursive_mutex_unlock(&mutex);
CHECK_EQ(recursive_mutex_is_locked(&mutex), false);
recursive_mutex_destruct(&mutex);
}
TEST_CASE("recursive_mutex_lock in another task should block when a lock is active") {
static int task_lock_counter = 0;
RecursiveMutex mutex = { 0 };
task_lock_counter = 0;
recursive_mutex_construct(&mutex);
recursive_mutex_lock(&mutex);
TaskHandle_t task_handle;
auto task_create_result = xTaskCreate(
[](void* input) {
RecursiveMutex* mutex_ptr = static_cast<RecursiveMutex*>(input);
recursive_mutex_lock(mutex_ptr);
task_lock_counter++;
vTaskDelete(nullptr);
},
"mutex_test",
2048,
&mutex,
0,
&task_handle
);
CHECK_EQ(task_create_result, pdPASS);
CHECK_EQ(task_lock_counter, 0);
recursive_mutex_unlock(&mutex);
vTaskDelay(2); // 1 is sufficient most of the time, but not always
CHECK_EQ(task_lock_counter, 1);
recursive_mutex_destruct(&mutex);
}
+112
View File
@@ -0,0 +1,112 @@
#include "doctest.h"
#include <tactility/delay.h>
#include <tactility/concurrent/thread.h>
TEST_CASE("when a thread is started then its callback should be called") {
bool has_called = false;
auto* thread = thread_alloc_full(
"immediate return task",
4096,
[](void* context) {
auto* has_called_ptr = static_cast<bool*>(context);
*has_called_ptr = true;
return 0;
},
&has_called,
-1
);
CHECK(!has_called);
CHECK_EQ(thread_start(thread), ERROR_NONE);
CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE);
thread_free(thread);
CHECK(has_called);
}
TEST_CASE("a thread can be started and stopped") {
bool interrupted = false;
auto* thread = thread_alloc_full(
"interruptable thread",
4096,
[](void* context) {
auto* interrupted_ptr = static_cast<bool*>(context);
while (!*interrupted_ptr) {
delay_millis(1);
}
return 0;
},
&interrupted,
-1
);
CHECK(thread);
CHECK_EQ(thread_start(thread), ERROR_NONE);
interrupted = true;
CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE);
thread_free(thread);
}
TEST_CASE("thread id should only be set at when thread is started") {
bool interrupted = false;
auto* thread = thread_alloc_full(
"interruptable thread",
4096,
[](void* context) {
auto* interrupted_ptr = static_cast<bool*>(context);
while (!*interrupted_ptr) {
delay_millis(1);
}
return 0;
},
&interrupted,
-1
);
CHECK_EQ(thread_get_task_handle(thread), nullptr);
CHECK_EQ(thread_start(thread), ERROR_NONE);
CHECK_NE(thread_get_task_handle(thread), nullptr);
interrupted = true;
CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE);
CHECK_EQ(thread_get_task_handle(thread), nullptr);
thread_free(thread);
}
TEST_CASE("thread state should be correct") {
bool interrupted = false;
auto* thread = thread_alloc_full(
"interruptable thread",
4096,
[](void* context) {
auto* interrupted_ptr = static_cast<bool*>(context);
while (!*interrupted_ptr) {
delay_millis(1);
}
return 0;
},
&interrupted,
-1
);
CHECK_EQ(thread_get_state(thread), THREAD_STATE_STOPPED);
thread_start(thread);
auto state = thread_get_state(thread);
CHECK((state == THREAD_STATE_STARTING || state == THREAD_STATE_RUNNING));
interrupted = true;
CHECK_EQ(thread_join(thread, 10, 1), ERROR_NONE);
CHECK_EQ(thread_get_state(thread), THREAD_STATE_STOPPED);
thread_free(thread);
}
TEST_CASE("thread id should only be set at when thread is started") {
auto* thread = thread_alloc_full(
"return code",
4096,
[](void* context) { return 123; },
nullptr,
-1
);
CHECK_EQ(thread_start(thread), ERROR_NONE);
CHECK_EQ(thread_join(thread, 1, 1), ERROR_NONE);
CHECK_EQ(thread_get_return_code(thread), 123);
thread_free(thread);
}
@@ -0,0 +1,30 @@
#include "doctest.h"
#include <tactility/delay.h>
#include <tactility/time.h>
TEST_CASE("delay ticks should be accurate within 1 tick") {
auto start_time = get_ticks();
delay_ticks(100);
auto end_time = get_ticks();
auto difference = end_time - start_time;
CHECK_EQ(difference >= 100, true);
CHECK_EQ(difference <= 101, true);
}
TEST_CASE("delay millis should be accurate within 1 tick") {
auto start_time = get_millis();
delay_millis(100);
auto end_time = get_millis();
auto difference = end_time - start_time;
CHECK_EQ(difference >= 100, true);
CHECK_EQ(difference <= 101, true);
}
TEST_CASE("microsecond time should be accurate within 1 tick") {
auto start_time = get_micros_since_boot();
delay_millis(100);
auto end_time = get_micros_since_boot();
auto difference = (end_time - start_time) / 1000;
CHECK_EQ(difference >= 99, true);
CHECK_EQ(difference <= 101, true);
}
+147
View File
@@ -0,0 +1,147 @@
#include "doctest.h"
#include <atomic>
#include <tactility/concurrent/timer.h>
#include <tactility/delay.h>
TEST_CASE("timer_alloc and timer_free should handle allocation and deallocation") {
auto callback = [](void* context) {};
struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, callback, nullptr);
CHECK_NE(timer, nullptr);
timer_free(timer);
}
TEST_CASE("timer_start and timer_stop should change running state") {
auto callback = [](void* context) {};
struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, callback, nullptr);
REQUIRE_NE(timer, nullptr);
CHECK_EQ(timer_is_running(timer), false);
CHECK_EQ(timer_start(timer), ERROR_NONE);
CHECK_EQ(timer_is_running(timer), true);
CHECK_EQ(timer_stop(timer), ERROR_NONE);
CHECK_EQ(timer_is_running(timer), false);
timer_free(timer);
}
TEST_CASE("one-shot timer should fire callback once") {
std::atomic<int> call_count{0};
struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) {
auto* count = static_cast<std::atomic<int>*>(context);
(*count)++;
}, &call_count);
REQUIRE_NE(timer, nullptr);
CHECK_EQ(timer_start(timer), ERROR_NONE);
delay_millis(20);
CHECK_EQ(call_count.load(), 1);
CHECK_EQ(timer_is_running(timer), false);
timer_free(timer);
}
TEST_CASE("periodic timer should fire callback multiple times") {
std::atomic<int> call_count{0};
struct Timer* timer = timer_alloc(TIMER_TYPE_PERIODIC, 10, [](void* context) {
auto* count = static_cast<std::atomic<int>*>(context);
(*count)++;
}, &call_count);
REQUIRE_NE(timer, nullptr);
CHECK_EQ(timer_start(timer), ERROR_NONE);
delay_millis(35); // Should fire around 3 times
CHECK_GE(call_count.load(), 3);
CHECK_EQ(timer_is_running(timer), true);
timer_stop(timer);
timer_free(timer);
}
TEST_CASE("timer_reset should restart the timer") {
std::atomic<int> call_count{0};
struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 20, [](void* context) {
auto* count = static_cast<std::atomic<int>*>(context);
(*count)++;
}, &call_count);
REQUIRE_NE(timer, nullptr);
CHECK_EQ(timer_start(timer), ERROR_NONE);
delay_millis(10);
CHECK_EQ(call_count.load(), 0);
// Resetting should push the expiry further
CHECK_EQ(timer_reset(timer), ERROR_NONE);
delay_millis(15);
CHECK_EQ(call_count.load(), 0); // Still shouldn't have fired if reset worked
delay_millis(10);
CHECK_EQ(call_count.load(), 1); // Now it should have fired
timer_free(timer);
}
TEST_CASE("timer_reset_with_interval should change the period") {
std::atomic<int> call_count{0};
struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 40, [](void* context) {
auto* count = static_cast<std::atomic<int>*>(context);
(*count)++;
}, &call_count);
REQUIRE_NE(timer, nullptr);
CHECK_EQ(timer_start(timer), ERROR_NONE);
// Change to a much shorter interval
CHECK_EQ(timer_reset_with_interval(timer, 10), ERROR_NONE);
delay_millis(20);
CHECK_EQ(call_count.load(), 1);
timer_free(timer);
}
TEST_CASE("timer_get_expiry_time should return a valid time") {
struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) {}, nullptr);
REQUIRE_NE(timer, nullptr);
timer_start(timer);
TickType_t expiry = timer_get_expiry_time(timer);
// Expiry should be in the future
CHECK_GT(expiry, xTaskGetTickCount());
timer_free(timer);
}
TEST_CASE("timer_set_pending_callback should execute callback in timer task") {
std::atomic<bool> called{false};
struct Context {
std::atomic<bool>* called;
uint32_t expected_arg;
uint32_t received_arg;
} context = { &called, 0x12345678, 0 };
auto pending_cb = [](void* ctx, uint32_t arg) {
auto* c = static_cast<Context*>(ctx);
c->received_arg = arg;
c->called->store(true);
};
// timer_set_pending_callback doesn't actually use the timer object in current implementation
// but we need one for the API
struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) {}, nullptr);
CHECK_EQ(timer_set_pending_callback(timer, pending_cb, &context, context.expected_arg, portMAX_DELAY), ERROR_NONE);
// Wait for timer task to process the callback
int retries = 10;
while (!called.load() && retries-- > 0) {
delay_millis(10);
}
CHECK(called.load());
CHECK_EQ(context.received_arg, context.expected_arg);
timer_free(timer);
}