Files
tactility/TactilityKernel/tests/source/file_mutex_test.cpp
T
Ken Van Hoeylandt db48dfe812 Fixes and improvements (#620)
- Standardized keyboard input using Unicode-based key codes across supported devices and the simulator. Keyboards don't emit `LV_KEY_*` anymore.
- Refactored lilygo encoder driver into a reusable GPIO rotary encoder driver (see `Drivers/gpio-encoder-module/`). Added more features to the config file.
- Improved LVGL keyboard device management, including duplicate prevention and reliable reconnects.
- LVGL file mutex now registers with lvgl start/stop
- Improved LVGL startup/shutdown stability and memory allocation reliability.
- Increased simulator LVGL memory capacity and improved USB device-class handling.
2026-08-23 17:21:16 +02:00

191 lines
6.1 KiB
C++

#include "doctest.h"
#include <tactility/filesystem/file_mutex.h>
namespace {
int lock_calls = 0;
int unlock_calls = 0;
int try_lock_calls = 0;
bool try_lock_result = true;
uint32_t try_lock_timeout_seen = 0;
void mock_lock() { lock_calls++; }
void mock_unlock() { unlock_calls++; }
bool mock_try_lock(uint32_t timeout) {
try_lock_calls++;
try_lock_timeout_seen = timeout;
return try_lock_result;
}
int lock_a_calls = 0;
int lock_b_calls = 0;
void mock_lock_a() { lock_a_calls++; }
void mock_lock_b() { lock_b_calls++; }
void reset_mocks() {
lock_calls = 0;
unlock_calls = 0;
try_lock_calls = 0;
try_lock_result = true;
try_lock_timeout_seen = 0;
lock_a_calls = 0;
lock_b_calls = 0;
}
} // namespace
TEST_CASE("file_mutex_get with zero registrations returns a no-op mutex") {
FileMutex mutex;
file_mutex_get(&mutex, "/nowhere/file.txt");
CHECK_EQ(mutex.lock, nullptr);
CHECK_EQ(mutex.try_lock, nullptr);
CHECK_EQ(mutex.unlock, nullptr);
// Calling through a no-op mutex must be safe, and try_lock must report success.
file_mutex_lock(&mutex);
CHECK_EQ(file_mutex_try_lock(&mutex, 123), true);
file_mutex_unlock(&mutex);
}
TEST_CASE("file_mutex_add/get with a single registration") {
reset_mocks();
FileMutex registered = { .lock = mock_lock, .try_lock = mock_try_lock, .unlock = mock_unlock };
file_mutex_add(&registered, "/mock1");
FileMutex mutex;
// Exact mount path match.
file_mutex_get(&mutex, "/mock1");
CHECK_EQ(mutex.lock, mock_lock);
CHECK_EQ(mutex.try_lock, mock_try_lock);
CHECK_EQ(mutex.unlock, mock_unlock);
// Descendant path match.
file_mutex_get(&mutex, "/mock1/nested/file.txt");
CHECK_EQ(mutex.lock, mock_lock);
// Unrelated path falls back to no-op.
FileMutex unrelated;
file_mutex_get(&unrelated, "/other/file.txt");
CHECK_EQ(unrelated.lock, nullptr);
// Prefix-but-not-descendant path (e.g. "/mock1x") must not match "/mock1".
FileMutex prefix_only;
file_mutex_get(&prefix_only, "/mock1x/file.txt");
CHECK_EQ(prefix_only.lock, nullptr);
// Exercise the resolved callbacks.
file_mutex_get(&mutex, "/mock1");
file_mutex_lock(&mutex);
CHECK_EQ(lock_calls, 1);
CHECK_EQ(file_mutex_try_lock(&mutex, 42), true);
CHECK_EQ(try_lock_calls, 1);
CHECK_EQ(try_lock_timeout_seen, 42);
file_mutex_unlock(&mutex);
CHECK_EQ(unlock_calls, 1);
// Re-registering the same path is a no-op: original callbacks remain in place.
FileMutex replacement = { .lock = nullptr, .try_lock = nullptr, .unlock = nullptr };
file_mutex_add(&replacement, "/mock1");
file_mutex_get(&mutex, "/mock1");
CHECK_EQ(mutex.lock, mock_lock);
}
TEST_CASE("file_mutex_add/get with two registrations resolves to the matching path") {
reset_mocks();
FileMutex mutex_a = { .lock = mock_lock_a, .try_lock = nullptr, .unlock = nullptr };
FileMutex mutex_b = { .lock = mock_lock_b, .try_lock = nullptr, .unlock = nullptr };
file_mutex_add(&mutex_a, "/mock2a");
file_mutex_add(&mutex_b, "/mock2b");
FileMutex resolved;
file_mutex_get(&resolved, "/mock2a/file.txt");
CHECK_EQ(resolved.lock, mock_lock_a);
file_mutex_get(&resolved, "/mock2b/file.txt");
CHECK_EQ(resolved.lock, mock_lock_b);
// Path matching neither registration falls back to no-op.
file_mutex_get(&resolved, "/mock2c/file.txt");
CHECK_EQ(resolved.lock, nullptr);
// Registration order matters: the first matching entry wins, not the longest
// prefix. A mount nested under an earlier one is shadowed by it.
FileMutex mutex_nested = { .lock = nullptr, .try_lock = nullptr, .unlock = nullptr };
file_mutex_add(&mutex_nested, "/mock2a/nested");
file_mutex_get(&resolved, "/mock2a/nested/file.txt");
CHECK_EQ(resolved.lock, mock_lock_a); // still /mock2a, registered first
}
TEST_CASE("file_mutex_add returns a valid id") {
reset_mocks();
FileMutex registered = { .lock = mock_lock, .try_lock = nullptr, .unlock = nullptr };
FileMutexId id = file_mutex_add(&registered, "/mockA");
CHECK_NE(id, FILE_MUTEX_ID_INVALID);
}
TEST_CASE("file_mutex_add with a duplicate path returns the existing id") {
reset_mocks();
FileMutex mutex_1 = { .lock = mock_lock, .try_lock = nullptr, .unlock = nullptr };
FileMutex mutex_2 = { .lock = mock_lock_a, .try_lock = nullptr, .unlock = nullptr };
FileMutexId id_1 = file_mutex_add(&mutex_1, "/mockB");
FileMutexId id_2 = file_mutex_add(&mutex_2, "/mockB");
CHECK_EQ(id_1, id_2);
FileMutex resolved;
file_mutex_get(&resolved, "/mockB");
CHECK_EQ(resolved.lock, mock_lock); // first registration's callbacks win, unchanged
}
TEST_CASE("file_mutex_remove removes a registration") {
reset_mocks();
FileMutex registered = { .lock = mock_lock, .try_lock = nullptr, .unlock = nullptr };
FileMutexId id = file_mutex_add(&registered, "/mockC");
FileMutex before;
file_mutex_get(&before, "/mockC");
CHECK_EQ(before.lock, mock_lock);
file_mutex_remove(id);
FileMutex after;
file_mutex_get(&after, "/mockC");
CHECK_EQ(after.lock, nullptr);
}
TEST_CASE("file_mutex_remove with an unknown id is a safe no-op") {
reset_mocks();
FileMutex registered = { .lock = mock_lock, .try_lock = nullptr, .unlock = nullptr };
file_mutex_add(&registered, "/mockD");
file_mutex_remove(999999); // never issued
file_mutex_remove(FILE_MUTEX_ID_INVALID);
FileMutex resolved;
file_mutex_get(&resolved, "/mockD");
CHECK_EQ(resolved.lock, mock_lock); // untouched
}
TEST_CASE("file_mutex_remove of one registration leaves others intact") {
reset_mocks();
FileMutex mutex_a = { .lock = mock_lock_a, .try_lock = nullptr, .unlock = nullptr };
FileMutex mutex_b = { .lock = mock_lock_b, .try_lock = nullptr, .unlock = nullptr };
FileMutexId id_a = file_mutex_add(&mutex_a, "/mockE1");
file_mutex_add(&mutex_b, "/mockE2");
file_mutex_remove(id_a);
FileMutex resolved_a;
file_mutex_get(&resolved_a, "/mockE1");
CHECK_EQ(resolved_a.lock, nullptr);
FileMutex resolved_b;
file_mutex_get(&resolved_b, "/mockE2");
CHECK_EQ(resolved_b.lock, mock_lock_b);
}