Move tests to relevant subprojects (#615)

- Moved test projects to the parent project they belong to
- Improved test stability/corectness
- Improved recursive directory deletion by safely ignoring current- and parent-directory entries.
- Update docs
This commit is contained in:
Ken Van Hoeylandt
2026-08-13 23:01:12 +02:00
committed by GitHub
parent d6b1d15e56
commit f943c4dd69
64 changed files with 137 additions and 1539 deletions
@@ -0,0 +1,18 @@
project(ServiceModuleTests)
enable_language(C CXX ASM)
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})
add_test(NAME ServiceModuleTests COMMAND ServiceModuleTests)
target_link_libraries(ServiceModuleTests PUBLIC
TactilityKernel
service-module
platform-posix
freertos_kernel
)
@@ -0,0 +1,53 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include "FreeRTOS.h"
#include "task.h"
typedef struct {
int argc;
char** argv;
int result;
} TestTaskData;
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
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
);
if (task_result != pdPASS) {
return 1;
}
vTaskStartScheduler();
return data.result;
}
@@ -0,0 +1,71 @@
#include "doctest.h"
#include <service/paths.h>
#include <tactility/paths.h>
#include <cstring>
#include <string>
TEST_CASE("paths_get_user_data_path returns a non-empty path") {
char buffer[192];
CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_NONE);
CHECK_GT(std::strlen(buffer), 0);
}
TEST_CASE("paths_get_user_data_path reports overflow for a too-small buffer") {
char buffer[1];
CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
}
TEST_CASE("service_paths_get_user_data_directory includes the service id") {
char root[192];
REQUIRE_EQ(paths_get_user_data_path(root, sizeof(root)), ERROR_NONE);
char buffer[224];
CHECK_EQ(service_paths_get_user_data_directory("my-service", buffer, sizeof(buffer)), ERROR_NONE);
std::string expected = std::string(root) + "/service/my-service";
CHECK_EQ(std::string(buffer), expected);
}
TEST_CASE("service_paths_get_user_data_path appends the child path") {
char directory[224];
REQUIRE_EQ(service_paths_get_user_data_directory("my-service", directory, sizeof(directory)), ERROR_NONE);
char buffer[256];
CHECK_EQ(service_paths_get_user_data_path("my-service", "settings.properties", buffer, sizeof(buffer)), ERROR_NONE);
std::string expected = std::string(directory) + "/settings.properties";
CHECK_EQ(std::string(buffer), expected);
}
TEST_CASE("service_paths_get_assets_directory is nested under the user data directory") {
char directory[224];
REQUIRE_EQ(service_paths_get_user_data_directory("my-service", directory, sizeof(directory)), ERROR_NONE);
char buffer[256];
CHECK_EQ(service_paths_get_assets_directory("my-service", buffer, sizeof(buffer)), ERROR_NONE);
std::string expected = std::string(directory) + "/assets";
CHECK_EQ(std::string(buffer), expected);
}
TEST_CASE("service_paths_get_assets_path appends the child path") {
char directory[224];
REQUIRE_EQ(service_paths_get_assets_directory("my-service", directory, sizeof(directory)), ERROR_NONE);
char buffer[256];
CHECK_EQ(service_paths_get_assets_path("my-service", "icon.png", buffer, sizeof(buffer)), ERROR_NONE);
std::string expected = std::string(directory) + "/icon.png";
CHECK_EQ(std::string(buffer), expected);
}
TEST_CASE("service_paths functions report overflow for a too-small buffer") {
char buffer[1];
CHECK_EQ(service_paths_get_user_data_directory("my-service", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
CHECK_EQ(service_paths_get_user_data_path("my-service", "child", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
CHECK_EQ(service_paths_get_assets_directory("my-service", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
CHECK_EQ(service_paths_get_assets_path("my-service", "child", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
}
@@ -0,0 +1,169 @@
#include "doctest.h"
#include <service/manager.h>
// Defined in service_instance.cpp. Internal-only, exposed here to test try_get/put gating.
extern "C" void service_instance_set_state(ServiceInstance* instance, ServiceState state);
static int create_called = 0;
static int destroy_called = 0;
static int on_start_called = 0;
static int on_stop_called = 0;
static error_t on_start_result = ERROR_NONE;
static const ServiceManifest* last_create_manifest = nullptr;
static const ServiceManifest* last_destroy_manifest = nullptr;
static void* test_create_service(const ServiceManifest* manifest) {
create_called++;
last_create_manifest = manifest;
return nullptr;
}
static void test_destroy_service(const ServiceManifest* manifest, void*) {
destroy_called++;
last_destroy_manifest = manifest;
}
static error_t test_on_start(ServiceInstance*, void*) {
on_start_called++;
return on_start_result;
}
static void test_on_stop(ServiceInstance*, void*) {
on_stop_called++;
}
static void reset_counters() {
create_called = 0;
destroy_called = 0;
on_start_called = 0;
on_stop_called = 0;
on_start_result = ERROR_NONE;
last_create_manifest = nullptr;
last_destroy_manifest = nullptr;
}
TEST_CASE("ServiceInstance construction and destruction") {
reset_counters();
static const ServiceManifest manifest = {
.id = "instance-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service,
.on_start = test_on_start,
.on_stop = test_on_stop
};
ServiceInstance instance = { .manifest = nullptr, .data = nullptr, .internal = nullptr };
CHECK_EQ(service_instance_construct(&instance, &manifest), ERROR_NONE);
CHECK_NE(instance.internal, nullptr);
CHECK_EQ(instance.manifest, &manifest);
CHECK_EQ(create_called, 1);
CHECK_EQ(last_create_manifest, &manifest);
CHECK_EQ(service_instance_get_state(&instance), SERVICE_STATE_STOPPED);
CHECK_EQ(service_instance_destruct(&instance), ERROR_NONE);
CHECK_EQ(instance.internal, nullptr);
CHECK_EQ(destroy_called, 1);
CHECK_EQ(last_destroy_manifest, &manifest);
}
TEST_CASE("service_manager_add rejects duplicate ids") {
reset_counters();
static const ServiceManifest manifest = {
.id = "duplicate-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service
};
CHECK_EQ(service_manager_add(&manifest, false), ERROR_NONE);
CHECK_EQ(service_manager_add(&manifest, false), ERROR_INVALID_ARGUMENT);
CHECK_EQ(service_manager_remove("duplicate-test"), ERROR_NONE);
}
TEST_CASE("service_registration start/stop lifecycle") {
reset_counters();
static const ServiceManifest manifest = {
.id = "lifecycle-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service,
.on_start = test_on_start,
.on_stop = test_on_stop
};
CHECK_EQ(service_manager_add(&manifest, false), ERROR_NONE);
CHECK_EQ(service_manager_get_state("lifecycle-test"), SERVICE_STATE_STOPPED);
CHECK_EQ(service_manager_start("lifecycle-test"), ERROR_NONE);
CHECK_EQ(on_start_called, 1);
CHECK_EQ(service_manager_get_state("lifecycle-test"), SERVICE_STATE_STARTED);
CHECK_NE(service_manager_find_instance("lifecycle-test"), nullptr);
// Starting again while already started should fail
CHECK_EQ(service_manager_start("lifecycle-test"), ERROR_INVALID_STATE);
// Removing while running should fail
CHECK_EQ(service_manager_remove("lifecycle-test"), ERROR_INVALID_STATE);
CHECK_EQ(service_manager_stop("lifecycle-test"), ERROR_NONE);
CHECK_EQ(on_stop_called, 1);
CHECK_EQ(service_manager_get_state("lifecycle-test"), SERVICE_STATE_STOPPED);
CHECK_EQ(service_manager_find_instance("lifecycle-test"), nullptr);
// Stopping again while already stopped should fail
CHECK_EQ(service_manager_stop("lifecycle-test"), ERROR_NOT_FOUND);
CHECK_EQ(service_manager_remove("lifecycle-test"), ERROR_NONE);
}
TEST_CASE("service_manager_add with auto_start") {
reset_counters();
static const ServiceManifest manifest = {
.id = "auto-start-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service,
.on_start = test_on_start,
.on_stop = test_on_stop
};
CHECK_EQ(service_manager_add(&manifest, true), ERROR_NONE);
CHECK_EQ(on_start_called, 1);
CHECK_EQ(service_manager_get_state("auto-start-test"), SERVICE_STATE_STARTED);
CHECK_EQ(service_manager_stop("auto-start-test"), ERROR_NONE);
CHECK_EQ(service_manager_remove("auto-start-test"), ERROR_NONE);
}
TEST_CASE("service_manager_start failure leaves service stopped") {
reset_counters();
on_start_result = ERROR_RESOURCE;
static const ServiceManifest manifest = {
.id = "failing-start-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service,
.on_start = test_on_start,
.on_stop = test_on_stop
};
CHECK_EQ(service_manager_add(&manifest, false), ERROR_NONE);
CHECK_EQ(service_manager_start("failing-start-test"), ERROR_RESOURCE);
CHECK_EQ(service_manager_get_state("failing-start-test"), SERVICE_STATE_STOPPED);
CHECK_EQ(service_manager_find_instance("failing-start-test"), nullptr);
CHECK_EQ(service_manager_remove("failing-start-test"), ERROR_NONE);
}
TEST_CASE("service_registration lookup functions with unknown id") {
CHECK_EQ(service_manager_get_state("unknown-service-id"), SERVICE_STATE_STOPPED);
CHECK_EQ(service_manager_find_manifest("unknown-service-id"), nullptr);
CHECK_EQ(service_manager_find_instance("unknown-service-id"), nullptr);
CHECK_EQ(service_manager_start("unknown-service-id"), ERROR_NOT_FOUND);
CHECK_EQ(service_manager_stop("unknown-service-id"), ERROR_NOT_FOUND);
CHECK_EQ(service_manager_remove("unknown-service-id"), ERROR_NOT_FOUND);
}