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:
committed by
GitHub
parent
1757af859c
commit
79e43b093a
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user