Implement unit testing (#30)

- Create `test` and `tactility-core-tests` subprojects
- Created tests for `thread.c`
- Fixed issue with thread cleanup (see what I did there? :P)
- Removed functions from `thread.h` that did not exist anymore
- Updated `ideas.md`
This commit is contained in:
Ken Van Hoeylandt 2024-02-02 00:12:36 +01:00 committed by GitHub
parent 50e7fb92c8
commit 47377439dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 7391 additions and 121 deletions

20
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Tests
on: [push]
jobs:
Build-PC:
runs-on: ubuntu-latest
env:
SKIP_SDL: true
steps:
- name: Checkout repo
uses: actions/checkout@v2
with:
submodules: recursive
- name: Configure Project
uses: threeal/cmake-action@v1.3.0
- name: Prepare Project
run: cmake -S ./ -B build
- name: Build Tests
run: cmake --build build --target build-tests
- name: Run Tests
run: build/tests/tactility-core/tactility-core-tests --exit

View File

@ -45,10 +45,17 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
) )
add_subdirectory(libs/mbedtls) add_subdirectory(libs/mbedtls)
if (NOT DEFINED ENV{SKIP_SDL})
add_subdirectory(app-sim) add_subdirectory(app-sim)
endif()
add_subdirectory(tests)
# region SDL & LVGL # region SDL & LVGL
# TODO: This is a temporary skipping option for running unit tests
# TODO: Remove when github action for SDL is working again
if (NOT DEFINED ENV{SKIP_SDL})
find_package(SDL2 REQUIRED CONFIG) find_package(SDL2 REQUIRED CONFIG)
add_subdirectory(libs/lvgl) # Added as idf component for ESP and as library for other targets add_subdirectory(libs/lvgl) # Added as idf component for ESP and as library for other targets
@ -113,3 +120,4 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
# endregion SDL & LVGL # endregion SDL & LVGL
endif() endif()
endif()

View File

@ -1,13 +1,9 @@
# TODOs # TODOs
- Update `view_port` to use `ViewPort` as handle externally and `ViewPortData` internally - Update `view_port` to use `ViewPort` as handle externally and `ViewPortData` internally
- Replace FreeRTOS semaphore from `Loader` with internal `Mutex` - Replace FreeRTOS semaphore from `Loader` with internal `Mutex`
- Create unit tests for `tactility-core` and `tactility` (PC-only for now) - Create more unit tests for `tactility-core` and `tactility` (PC-only for now)
- Have a way to deinit LVGL drivers that are created from `HardwareConfig` - Have a way to deinit LVGL drivers that are created from `HardwareConfig`
- Thread is broken: `tt_thread_join()` always hangs because `tt_thread_cleanup_tcb_event()`
is not automatically called. This is normally done by a hook in `FreeRTOSConfig.h`
but that seems to not work with ESP32. I should investigate task cleanup hooks further.
- Set DPI in sdkconfig for Waveshare display - Set DPI in sdkconfig for Waveshare display
- Try to drive Yellow Board backlight with PWM to reduce backlight strength
- Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials. - Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials.
- Show a warning screen when a user plugs in the SD card on a device that only supports mounting at boot. - Show a warning screen when a user plugs in the SD card on a device that only supports mounting at boot.
- Try out Waveshare S3 120MHz mode for PSRAM (see "enabling 120M PSRAM is necessary" in [docs](https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-4.3#Other_Notes)) - Try out Waveshare S3 120MHz mode for PSRAM (see "enabling 120M PSRAM is necessary" in [docs](https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-4.3#Other_Notes))
@ -20,7 +16,6 @@ but that seems to not work with ESP32. I should investigate task cleanup hooks f
- If present, use LED to show boot status - If present, use LED to show boot status
# App Improvement Ideas # App Improvement Ideas
- Make a Settings app to show all the apps that have a "settings" app type (and hide those in desktop)
- Sort desktop apps by name. - Sort desktop apps by name.
- Light/dark mode selection in Display settings app. - Light/dark mode selection in Display settings app.

4
runtests.sh Executable file
View File

@ -0,0 +1,4 @@
cmake -S ./ -B build-sim
cmake --build build-sim --target build-tests -j 14
build-sim/tests/tactility-core/tactility-core-tests --exit

View File

@ -72,7 +72,7 @@ static void tt_thread_body(void* context) {
tt_assert(context); tt_assert(context);
Thread* thread = context; Thread* thread = context;
// store thread instance to thread local storage // Store thread instance to thread local storage
tt_assert(pvTaskGetThreadLocalStoragePointer(NULL, 0) == NULL); tt_assert(pvTaskGetThreadLocalStoragePointer(NULL, 0) == NULL);
vTaskSetThreadLocalStoragePointer(NULL, 0, thread); vTaskSetThreadLocalStoragePointer(NULL, 0, thread);
@ -80,19 +80,23 @@ static void tt_thread_body(void* context) {
tt_thread_set_state(thread, ThreadStateRunning); tt_thread_set_state(thread, ThreadStateRunning);
thread->ret = thread->callback(thread->context); thread->ret = thread->callback(thread->context);
TT_LOG_I(TAG, "thread returned: %s", thread->name ?: "[no name]");
tt_assert(thread->state == ThreadStateRunning); tt_assert(thread->state == ThreadStateRunning);
if (thread->is_static) { if (thread->is_static) {
TT_LOG_I( TT_LOG_I(
TAG, TAG,
"%s service thread TCB memory will not be reclaimed", "%s static task memory will not be reclaimed",
thread->name ? thread->name : "<unnamed service>" thread->name ? thread->name : "<unnamed service>"
); );
} }
tt_thread_set_state(thread, ThreadStateStopped); tt_thread_set_state(thread, ThreadStateStopped);
vTaskSetThreadLocalStoragePointer(NULL, 0, NULL);
thread->task_handle = NULL;
vTaskDelete(NULL); vTaskDelete(NULL);
tt_thread_catch(); tt_thread_catch();
} }
@ -114,7 +118,7 @@ Thread* tt_thread_alloc() {
tt_thread_set_appid(thread, "unknown"); tt_thread_set_appid(thread, "unknown");
} }
} else { } else {
// if scheduler is not started, we are starting driver thread // If scheduler is not started, we are starting driver thread
tt_thread_set_appid(thread, "driver"); tt_thread_set_appid(thread, "driver");
} }
@ -250,28 +254,18 @@ void tt_thread_start(Thread* thread) {
#else #else
TT_LOG_E(TAG, "static tasks are not supported by current FreeRTOS config/platform - creating regular one"); TT_LOG_E(TAG, "static tasks are not supported by current FreeRTOS config/platform - creating regular one");
BaseType_t ret = xTaskCreate( BaseType_t ret = xTaskCreate(
tt_thread_body, thread->name, stack, thread, priority, &thread->task_handle tt_thread_body, thread->name, stack, thread, priority, &(thread->task_handle)
); );
tt_check(ret == pdPASS); tt_check(ret == pdPASS);
#endif #endif
} else { } else {
BaseType_t ret = xTaskCreate( BaseType_t ret = xTaskCreate(
tt_thread_body, thread->name, stack, thread, priority, &thread->task_handle tt_thread_body, thread->name, stack, thread, priority, &(thread->task_handle)
); );
tt_check(ret == pdPASS); tt_check(ret == pdPASS);
} }
tt_check(thread->task_handle); tt_check(thread->state == ThreadStateStopped || thread->task_handle);
}
void tt_thread_cleanup_tcb_event(TaskHandle_t task) {
Thread* thread = pvTaskGetThreadLocalStoragePointer(task, 0);
if (thread) {
// clear thread local storage
vTaskSetThreadLocalStoragePointer(task, 0, NULL);
tt_assert(thread->task_handle == task);
thread->task_handle = NULL;
}
} }
bool tt_thread_join(Thread* thread) { bool tt_thread_join(Thread* thread) {

View File

@ -197,26 +197,6 @@ bool tt_thread_join(Thread* thread);
*/ */
ThreadId tt_thread_get_id(Thread* thread); ThreadId tt_thread_get_id(Thread* thread);
/** Enable heap tracing
*
* @param thread Thread instance
*/
void tt_thread_enable_heap_trace(Thread* thread);
/** Disable heap tracing
*
* @param thread Thread instance
*/
void tt_thread_disable_heap_trace(Thread* thread);
/** Get thread heap size
*
* @param thread Thread instance
*
* @return size in bytes
*/
size_t tt_thread_get_heap_size(Thread* thread);
/** Get thread return code /** Get thread return code
* *
* @param thread Thread instance * @param thread Thread instance
@ -276,33 +256,6 @@ const char* tt_thread_get_appid(ThreadId thread_id);
*/ */
uint32_t tt_thread_get_stack_space(ThreadId thread_id); uint32_t tt_thread_get_stack_space(ThreadId thread_id);
/** Get STDOUT callback for thead
*
* @return STDOUT callback
*/
ThreadStdoutWriteCallback tt_thread_get_stdout_callback();
/** Set STDOUT callback for thread
*
* @param callback callback or NULL to clear
*/
void tt_thread_set_stdout_callback(ThreadStdoutWriteCallback callback);
/** Write data to buffered STDOUT
*
* @param data input data
* @param size input data size
*
* @return size_t written data size
*/
size_t tt_thread_stdout_write(const char* data, size_t size);
/** Flush data to STDOUT
*
* @return int32_t error code
*/
int32_t tt_thread_stdout_flush();
/** Suspend thread /** Suspend thread
* *
* @param thread_id thread id * @param thread_id thread id

9
tests/CMakeLists.txt Normal file
View File

@ -0,0 +1,9 @@
project(tests)
set(DOCTESTINC ${PROJECT_SOURCE_DIR}/include)
enable_testing()
add_subdirectory(tactility-core)
add_custom_target(build-tests)
add_dependencies(build-tests tactility-core-tests)

7106
tests/include/doctest.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
project(tactility-core-tests)
enable_language(C CXX ASM)
set(CMAKE_CXX_COMPILER g++)
file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/*.cpp)
add_executable(tactility-core-tests EXCLUDE_FROM_ALL ${TEST_SOURCES})
target_include_directories(tactility-core-tests PRIVATE
${DOCTESTINC}
)
add_test(NAME tactility-core-tests
COMMAND tactility-core-tests
)
target_link_libraries(tactility-core-tests
PUBLIC tactility-core
freertos-kernel
)

View File

@ -0,0 +1,58 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cassert>
#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();
if (context.shouldExit()) { // important - query flags (and --exit) rely on the user doing this
vTaskEndScheduler();
}
vTaskDelete(NULL);
}
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,
NULL
);
assert(task_result == pdPASS);
vTaskStartScheduler();
}
extern "C" {
// Required for FreeRTOS
void vAssertCalled(unsigned long line, const char* const file) {
__assert_fail("assert failed", file, line, "");
}
}

View File

@ -0,0 +1,102 @@
#include "doctest.h"
#include "thread.h"
#include "FreeRTOS.h"
#include "task.h"
static int interruptable_thread(TT_UNUSED void* parameter) {
bool* interrupted = (bool*)parameter;
while (!*interrupted) {
vTaskDelay(5);
}
return 0;
}
static bool immedate_return_thread_called = false;
static int immediate_return_thread(TT_UNUSED void* parameter) {
immedate_return_thread_called = true;
return 0;
}
static int thread_with_return_code(TT_UNUSED void* parameter) {
int* code = (int*)parameter;
return *code;
}
TEST_CASE("when a thread is started then its callback should be called") {
Thread* thread = tt_thread_alloc_ex(
"immediate return task",
4096,
&immediate_return_thread,
NULL
);
CHECK(!immedate_return_thread_called);
tt_thread_start(thread);
tt_thread_join(thread);
tt_thread_free(thread);
CHECK(immedate_return_thread_called);
}
TEST_CASE("a thread can be started and stopped") {
bool interrupted = false;
Thread* thread = tt_thread_alloc_ex(
"interruptable thread",
4096,
&interruptable_thread,
&interrupted
);
CHECK(thread);
tt_thread_start(thread);
interrupted = true;
tt_thread_join(thread);
tt_thread_free(thread);
}
TEST_CASE("thread id should only be set at when thread is started") {
bool interrupted = false;
Thread* thread = tt_thread_alloc_ex(
"interruptable thread",
4096,
&interruptable_thread,
&interrupted
);
CHECK(tt_thread_get_id(thread) == NULL);
tt_thread_start(thread);
CHECK(tt_thread_get_id(thread) != NULL);
interrupted = true;
tt_thread_join(thread);
CHECK(tt_thread_get_id(thread) == NULL);
tt_thread_free(thread);
}
TEST_CASE("thread state should be correct") {
bool interrupted = false;
Thread* thread = tt_thread_alloc_ex(
"interruptable thread",
4096,
&interruptable_thread,
&interrupted
);
CHECK_EQ(tt_thread_get_state(thread), ThreadStateStopped);
tt_thread_start(thread);
ThreadState state = tt_thread_get_state(thread);
CHECK((state == ThreadStateStarting || state == ThreadStateRunning));
interrupted = true;
tt_thread_join(thread);
CHECK_EQ(tt_thread_get_state(thread), ThreadStateStopped);
tt_thread_free(thread);
}
TEST_CASE("thread id should only be set at when thread is started") {
int code = 123;
Thread* thread = tt_thread_alloc_ex(
"return code",
4096,
&thread_with_return_code,
&code
);
tt_thread_start(thread);
tt_thread_join(thread);
CHECK_EQ(tt_thread_get_return_code(thread), code);
tt_thread_free(thread);
}