Drivers, device migrations and other improvements (#566)
- RGB display driver added - display_driver API: - Added software-based display rotation for screens without hardware rotation support. - Improved display capability detection across supported display drivers. - lvgl_display driver: - Improved framebuffer handling for smoother, more reliable display updates. - Display gap and rotation behavior now adapts more accurately to hardware capabilities. - Migration Crowpanel 5" basic & advance to kernel drivers - Improve KernelDisplay app: brightness controls are hidden for displays that only support on/off operation - device.py now allows for (non-ambiguous) partial device IDs - TactilityKernel: Implement more tests
This commit is contained in:
committed by
GitHub
parent
bd8fdfd858
commit
6fb2bb736c
@@ -0,0 +1,119 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <tactility/device_listener.h>
|
||||
|
||||
// Declared in device_listener.cpp's private header; forward-declared here rather than
|
||||
// including the private header, matching the pattern used for other internal-only hooks.
|
||||
extern "C" void device_listener_notify(Device* dev, DeviceEvent event);
|
||||
|
||||
static std::vector<std::pair<void*, DeviceEvent>> calls_a;
|
||||
static std::vector<std::pair<void*, DeviceEvent>> calls_b;
|
||||
|
||||
static void listener_a(Device* dev, DeviceEvent event, void* context) {
|
||||
calls_a.push_back({ context, event });
|
||||
}
|
||||
|
||||
static void listener_b(Device* dev, DeviceEvent event, void* context) {
|
||||
calls_b.push_back({ context, event });
|
||||
}
|
||||
|
||||
static void reset_calls() {
|
||||
calls_a.clear();
|
||||
calls_b.clear();
|
||||
}
|
||||
|
||||
TEST_CASE("device_listener_notify invokes every registered listener with its own context") {
|
||||
reset_calls();
|
||||
int context_a = 1;
|
||||
int context_b = 2;
|
||||
|
||||
device_listener_add(listener_a, &context_a);
|
||||
device_listener_add(listener_b, &context_b);
|
||||
|
||||
auto* fake_device = reinterpret_cast<Device*>(0x1000);
|
||||
device_listener_notify(fake_device, DEVICE_EVENT_STARTED);
|
||||
|
||||
CHECK_EQ(calls_a.size(), 1);
|
||||
CHECK_EQ(calls_a[0].first, &context_a);
|
||||
CHECK_EQ(calls_a[0].second, DEVICE_EVENT_STARTED);
|
||||
|
||||
CHECK_EQ(calls_b.size(), 1);
|
||||
CHECK_EQ(calls_b[0].first, &context_b);
|
||||
CHECK_EQ(calls_b[0].second, DEVICE_EVENT_STARTED);
|
||||
|
||||
device_listener_remove(listener_a);
|
||||
device_listener_remove(listener_b);
|
||||
}
|
||||
|
||||
TEST_CASE("device_listener_remove stops further notifications for that callback only") {
|
||||
reset_calls();
|
||||
int context_a = 1;
|
||||
int context_b = 2;
|
||||
|
||||
device_listener_add(listener_a, &context_a);
|
||||
device_listener_add(listener_b, &context_b);
|
||||
|
||||
device_listener_remove(listener_a);
|
||||
|
||||
auto* fake_device = reinterpret_cast<Device*>(0x1000);
|
||||
device_listener_notify(fake_device, DEVICE_EVENT_STOPPED);
|
||||
|
||||
CHECK_EQ(calls_a.size(), 0);
|
||||
CHECK_EQ(calls_b.size(), 1);
|
||||
|
||||
device_listener_remove(listener_b);
|
||||
}
|
||||
|
||||
TEST_CASE("device_listener_remove on an unregistered callback is a no-op") {
|
||||
reset_calls();
|
||||
int context_b = 2;
|
||||
device_listener_add(listener_b, &context_b);
|
||||
|
||||
// listener_a was never added, so removing it must not disturb listener_b.
|
||||
device_listener_remove(listener_a);
|
||||
|
||||
auto* fake_device = reinterpret_cast<Device*>(0x1000);
|
||||
device_listener_notify(fake_device, DEVICE_EVENT_STARTED);
|
||||
|
||||
CHECK_EQ(calls_b.size(), 1);
|
||||
|
||||
device_listener_remove(listener_b);
|
||||
}
|
||||
|
||||
static bool reentrant_add_triggered = false;
|
||||
|
||||
static void reentrant_listener(Device* dev, DeviceEvent event, void* context) {
|
||||
calls_a.push_back({ context, event });
|
||||
if (!reentrant_add_triggered) {
|
||||
reentrant_add_triggered = true;
|
||||
// Adding a listener from within a notification must not deadlock: notify() takes a
|
||||
// snapshot of the listener list under the lock, then invokes callbacks after unlocking.
|
||||
device_listener_add(listener_b, context);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("device_listener_notify is safe when a listener adds another listener during notification") {
|
||||
reset_calls();
|
||||
reentrant_add_triggered = false;
|
||||
int context_a = 1;
|
||||
|
||||
device_listener_add(reentrant_listener, &context_a);
|
||||
|
||||
auto* fake_device = reinterpret_cast<Device*>(0x1000);
|
||||
device_listener_notify(fake_device, DEVICE_EVENT_STARTED);
|
||||
|
||||
// The listener added during this round of notification was not part of the snapshot,
|
||||
// so it should not have been invoked yet.
|
||||
CHECK_EQ(calls_a.size(), 1);
|
||||
CHECK_EQ(calls_b.size(), 0);
|
||||
|
||||
// A second round picks up the newly-added listener.
|
||||
device_listener_notify(fake_device, DEVICE_EVENT_STOPPED);
|
||||
CHECK_EQ(calls_a.size(), 2);
|
||||
CHECK_EQ(calls_b.size(), 1);
|
||||
|
||||
device_listener_remove(reentrant_listener);
|
||||
device_listener_remove(listener_b);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include <tactility/filesystem/file_system.h>
|
||||
|
||||
static int mount_called = 0;
|
||||
static int unmount_called = 0;
|
||||
static bool mounted_state = false;
|
||||
static error_t mount_result = ERROR_NONE;
|
||||
static error_t unmount_result = ERROR_NONE;
|
||||
|
||||
static error_t test_mount(void*) {
|
||||
mount_called++;
|
||||
mounted_state = true;
|
||||
return mount_result;
|
||||
}
|
||||
|
||||
static error_t test_unmount(void*) {
|
||||
unmount_called++;
|
||||
mounted_state = false;
|
||||
return unmount_result;
|
||||
}
|
||||
|
||||
static bool test_is_mounted(void*) {
|
||||
return mounted_state;
|
||||
}
|
||||
|
||||
static error_t test_get_path(void* data, char* out_path, size_t out_path_size) {
|
||||
const char* path = static_cast<const char*>(data);
|
||||
if (std::strlen(path) + 1 > out_path_size) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
std::strcpy(out_path, path);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const FileSystemApi test_api = {
|
||||
.mount = test_mount,
|
||||
.unmount = test_unmount,
|
||||
.is_mounted = test_is_mounted,
|
||||
.get_path = test_get_path
|
||||
};
|
||||
|
||||
static void reset_counters() {
|
||||
mount_called = 0;
|
||||
unmount_called = 0;
|
||||
mounted_state = false;
|
||||
mount_result = ERROR_NONE;
|
||||
unmount_result = ERROR_NONE;
|
||||
}
|
||||
|
||||
TEST_CASE("file_system_mount/unmount delegate to the api and reflect is_mounted state") {
|
||||
reset_counters();
|
||||
char path_data[] = "some/path";
|
||||
FileSystem* fs = file_system_add(&test_api, path_data);
|
||||
|
||||
CHECK_EQ(file_system_is_mounted(fs), false);
|
||||
CHECK_EQ(file_system_mount(fs), ERROR_NONE);
|
||||
CHECK_EQ(mount_called, 1);
|
||||
CHECK_EQ(file_system_is_mounted(fs), true);
|
||||
|
||||
CHECK_EQ(file_system_unmount(fs), ERROR_NONE);
|
||||
CHECK_EQ(unmount_called, 1);
|
||||
CHECK_EQ(file_system_is_mounted(fs), false);
|
||||
|
||||
file_system_remove(fs);
|
||||
}
|
||||
|
||||
TEST_CASE("file_system_mount propagates api failure without changing state on its own") {
|
||||
reset_counters();
|
||||
mount_result = ERROR_RESOURCE;
|
||||
char path_data[] = "some/path";
|
||||
FileSystem* fs = file_system_add(&test_api, path_data);
|
||||
|
||||
CHECK_EQ(file_system_mount(fs), ERROR_RESOURCE);
|
||||
// The fake api still flips mounted_state; file_system itself has no independent state,
|
||||
// it always defers to the api's is_mounted().
|
||||
CHECK_EQ(file_system_is_mounted(fs), true);
|
||||
|
||||
mounted_state = false;
|
||||
file_system_remove(fs);
|
||||
}
|
||||
|
||||
TEST_CASE("file_system_get_path forwards to the api with the caller's buffer size") {
|
||||
reset_counters();
|
||||
char path_data[] = "mount/point";
|
||||
FileSystem* fs = file_system_add(&test_api, path_data);
|
||||
|
||||
char small_buffer[4];
|
||||
CHECK_EQ(file_system_get_path(fs, small_buffer, sizeof(small_buffer)), ERROR_BUFFER_OVERFLOW);
|
||||
|
||||
char big_buffer[32];
|
||||
CHECK_EQ(file_system_get_path(fs, big_buffer, sizeof(big_buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(big_buffer, "mount/point"), 0);
|
||||
|
||||
file_system_remove(fs);
|
||||
}
|
||||
|
||||
TEST_CASE("file_system_set_owner/get_owner round-trip and default to null") {
|
||||
reset_counters();
|
||||
char path_data[] = "some/path";
|
||||
FileSystem* fs = file_system_add(&test_api, path_data);
|
||||
|
||||
CHECK_EQ(file_system_get_owner(fs), nullptr);
|
||||
|
||||
auto* fake_owner = reinterpret_cast<Device*>(0x1234);
|
||||
file_system_set_owner(fs, fake_owner);
|
||||
CHECK_EQ(file_system_get_owner(fs), fake_owner);
|
||||
|
||||
file_system_set_owner(fs, nullptr);
|
||||
CHECK_EQ(file_system_get_owner(fs), nullptr);
|
||||
|
||||
file_system_remove(fs);
|
||||
}
|
||||
|
||||
TEST_CASE("file_system_for_each visits every registered file system") {
|
||||
reset_counters();
|
||||
char path_a[] = "a";
|
||||
char path_b[] = "b";
|
||||
char path_c[] = "c";
|
||||
FileSystem* fs_a = file_system_add(&test_api, path_a);
|
||||
FileSystem* fs_b = file_system_add(&test_api, path_b);
|
||||
FileSystem* fs_c = file_system_add(&test_api, path_c);
|
||||
|
||||
std::vector<FileSystem*> visited;
|
||||
file_system_for_each(&visited, [](FileSystem* fs, void* context) {
|
||||
static_cast<std::vector<FileSystem*>*>(context)->push_back(fs);
|
||||
return true;
|
||||
});
|
||||
|
||||
CHECK_EQ(visited.size(), 3);
|
||||
CHECK(std::find(visited.begin(), visited.end(), fs_a) != visited.end());
|
||||
CHECK(std::find(visited.begin(), visited.end(), fs_b) != visited.end());
|
||||
CHECK(std::find(visited.begin(), visited.end(), fs_c) != visited.end());
|
||||
|
||||
file_system_remove(fs_a);
|
||||
file_system_remove(fs_b);
|
||||
file_system_remove(fs_c);
|
||||
}
|
||||
|
||||
TEST_CASE("file_system_for_each stops early when the callback returns false") {
|
||||
reset_counters();
|
||||
char path_a[] = "a";
|
||||
char path_b[] = "b";
|
||||
FileSystem* fs_a = file_system_add(&test_api, path_a);
|
||||
FileSystem* fs_b = file_system_add(&test_api, path_b);
|
||||
|
||||
int visit_count = 0;
|
||||
file_system_for_each(&visit_count, [](FileSystem*, void* context) {
|
||||
(*static_cast<int*>(context))++;
|
||||
return false; // stop after the first entry
|
||||
});
|
||||
|
||||
CHECK_EQ(visit_count, 1);
|
||||
|
||||
file_system_remove(fs_a);
|
||||
file_system_remove(fs_b);
|
||||
}
|
||||
|
||||
TEST_CASE("file_system_remove drops the file system from subsequent iteration") {
|
||||
reset_counters();
|
||||
char path_a[] = "a";
|
||||
char path_b[] = "b";
|
||||
FileSystem* fs_a = file_system_add(&test_api, path_a);
|
||||
FileSystem* fs_b = file_system_add(&test_api, path_b);
|
||||
|
||||
file_system_remove(fs_a);
|
||||
|
||||
std::vector<FileSystem*> visited;
|
||||
file_system_for_each(&visited, [](FileSystem* fs, void* context) {
|
||||
static_cast<std::vector<FileSystem*>*>(context)->push_back(fs);
|
||||
return true;
|
||||
});
|
||||
|
||||
CHECK_EQ(visited.size(), 1);
|
||||
CHECK_EQ(visited[0], fs_b);
|
||||
|
||||
file_system_remove(fs_b);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <tactility/paths.h>
|
||||
|
||||
// The simulator target is never built with ESP_PLATFORM, so paths_get_user_data_path()
|
||||
// always takes the fixed "data" path branch here, guarded by a buffer-size check.
|
||||
|
||||
TEST_CASE("paths_get_user_data_path succeeds when the buffer exactly fits") {
|
||||
char buffer[16] = { 0 };
|
||||
CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "data"), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("paths_get_user_data_path succeeds with a buffer sized to exactly fit the string and terminator") {
|
||||
char buffer[5] = { 0 }; // strlen("data") + 1
|
||||
CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "data"), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("paths_get_user_data_path reports a buffer overflow when the buffer is one byte too small") {
|
||||
char buffer[4] = { 0 }; // strlen("data"), no room for the terminator
|
||||
CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
|
||||
}
|
||||
|
||||
TEST_CASE("paths_get_user_data_path reports a buffer overflow for a zero-size buffer") {
|
||||
char buffer[1] = { 'x' };
|
||||
CHECK_EQ(paths_get_user_data_path(buffer, 0), ERROR_BUFFER_OVERFLOW);
|
||||
CHECK_EQ(buffer[0], 'x'); // untouched
|
||||
}
|
||||
Reference in New Issue
Block a user