#include "doctest.h" #include #include #include #include static Module keyboard_test_module = { .name = "keyboard_test_module", .start = nullptr, .stop = nullptr }; static int read_key_call_count = 0; // First call reports a key press, every call after that reports nothing pending (key == 0), // matching the convention every real driver uses (see e.g. sdl_keyboard_read_key). static error_t fake_read_key(Device*, KeyboardKeyData* data) { read_key_call_count++; if (read_key_call_count == 1) { data->key = 'a'; data->pressed = true; data->continue_reading = false; } else { data->key = 0; data->pressed = false; data->continue_reading = false; } return ERROR_NONE; } static const KeyboardApi fake_keyboard_api = { .read_key = fake_read_key, .get_backlight = nullptr, .is_present = nullptr, }; static Driver fake_keyboard_driver = { .name = "fake_keyboard_driver", .compatible = (const char*[]) { "keyboard_test,fake", nullptr }, .start_device = nullptr, .stop_device = nullptr, .api = &fake_keyboard_api, .device_type = &KEYBOARD_TYPE, .owner = &keyboard_test_module, .internal = nullptr, }; // No read_key: this driver only ever pushes events via keyboard_emit_key(). static const KeyboardApi fake_emit_only_keyboard_api = { .read_key = nullptr, .get_backlight = nullptr, .is_present = nullptr, }; static Driver fake_emit_only_keyboard_driver = { .name = "fake_emit_only_keyboard_driver", .compatible = (const char*[]) { "keyboard_test,fake_emit_only", nullptr }, .start_device = nullptr, .stop_device = nullptr, .api = &fake_emit_only_keyboard_api, .device_type = &KEYBOARD_TYPE, .owner = &keyboard_test_module, .internal = nullptr, }; TEST_CASE("keyboard_read_key returns ERROR_INVALID_STATE, without touching the driver, on a device that isn't started") { read_key_call_count = 0; static Device fake_device { .name = "fake_keyboard_device_not_started", .config = nullptr, .parent = nullptr, }; CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE); CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE); // Deliberately not started - a hotplug-managed keyboard can be constructed+added long before // (or after) its driver is actually running, but LVGL's indev still polls it unconditionally. KeyboardKeyData out { .key = 'x' }; CHECK_EQ(keyboard_read_key(&fake_device, &out), ERROR_INVALID_STATE); CHECK_EQ(out.key, 0); CHECK_EQ(read_key_call_count, 0); CHECK_EQ(device_remove(&fake_device), ERROR_NONE); CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE); } TEST_CASE("keyboard_poll's default implementation fans out a read_key result to other subscribers") { read_key_call_count = 0; static Device fake_device { .name = "fake_keyboard_device", .config = nullptr, .parent = nullptr, }; CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE); CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE); CHECK_EQ(device_start(&fake_device), ERROR_NONE); KeyboardEventSubscription sub_a {}; KeyboardEventSubscription sub_b {}; CHECK_EQ(keyboard_subscribe(&fake_device, &sub_a), ERROR_NONE); CHECK_EQ(keyboard_subscribe(&fake_device, &sub_b), ERROR_NONE); KeyboardKeyData out {}; CHECK_EQ(keyboard_poll(&fake_device, &sub_a, &out), ERROR_NONE); CHECK_EQ(out.key, 'a'); CHECK_EQ(read_key_call_count, 1); // sub_b receives the same event without a second hardware read - it was fanned out by sub_a's poll. CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_NONE); CHECK_EQ(out.key, 'a'); CHECK_EQ(read_key_call_count, 1); // sub_b's queue is now empty; polling again falls through to a real (empty) hardware read. CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_TIMEOUT); CHECK_EQ(read_key_call_count, 2); CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_a), ERROR_NONE); CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_b), ERROR_NONE); CHECK_EQ(device_stop(&fake_device), ERROR_NONE); CHECK_EQ(device_remove(&fake_device), ERROR_NONE); CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE); } TEST_CASE("keyboard_read_key fans out to subscribers even when called directly") { read_key_call_count = 0; static Device fake_device { .name = "fake_keyboard_device_read_key_fanout", .config = nullptr, .parent = nullptr, }; CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE); CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE); CHECK_EQ(device_start(&fake_device), ERROR_NONE); KeyboardEventSubscription sub {}; CHECK_EQ(keyboard_subscribe(&fake_device, &sub), ERROR_NONE); KeyboardKeyData out {}; CHECK_EQ(keyboard_read_key(&fake_device, &out), ERROR_NONE); CHECK_EQ(out.key, 'a'); // sub never called keyboard_poll() itself - it received this via keyboard_read_key()'s fan-out. KeyboardKeyData polled {}; CHECK_EQ(keyboard_poll(&fake_device, &sub, &polled), ERROR_NONE); CHECK_EQ(polled.key, 'a'); CHECK_EQ(read_key_call_count, 1); CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub), ERROR_NONE); CHECK_EQ(device_stop(&fake_device), ERROR_NONE); CHECK_EQ(device_remove(&fake_device), ERROR_NONE); CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE); } TEST_CASE("keyboard_emit_key delivers to subscribers of a driver with no read_key") { static Device fake_device { .name = "fake_emit_only_keyboard_device", .config = nullptr, .parent = nullptr, }; CHECK_EQ(driver_construct_add(&fake_emit_only_keyboard_driver), ERROR_NONE); CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake_emit_only"), ERROR_NONE); CHECK_EQ(device_start(&fake_device), ERROR_NONE); KeyboardEventSubscription sub {}; CHECK_EQ(keyboard_subscribe(&fake_device, &sub), ERROR_NONE); // Nothing pushed yet, and there's no read_key to fall back on. KeyboardKeyData out {}; CHECK_EQ(keyboard_poll(&fake_device, &sub, &out), ERROR_TIMEOUT); KeyboardKeyData emitted { .key = 'z', .pressed = true, .continue_reading = false }; keyboard_emit_key(&fake_device, emitted); CHECK_EQ(keyboard_poll(&fake_device, &sub, &out), ERROR_NONE); CHECK_EQ(out.key, 'z'); CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub), ERROR_NONE); CHECK_EQ(device_stop(&fake_device), ERROR_NONE); CHECK_EQ(device_remove(&fake_device), ERROR_NONE); CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); CHECK_EQ(driver_remove_destruct(&fake_emit_only_keyboard_driver), ERROR_NONE); } TEST_CASE("keyboard_unsubscribe stops a subscription from receiving further fanned-out events") { read_key_call_count = 0; static Device fake_device { .name = "fake_keyboard_device_2", .config = nullptr, .parent = nullptr, }; CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE); CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE); CHECK_EQ(device_start(&fake_device), ERROR_NONE); KeyboardEventSubscription sub_a {}; KeyboardEventSubscription sub_b {}; CHECK_EQ(keyboard_subscribe(&fake_device, &sub_a), ERROR_NONE); CHECK_EQ(keyboard_subscribe(&fake_device, &sub_b), ERROR_NONE); CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_b), ERROR_NONE); KeyboardKeyData out {}; CHECK_EQ(keyboard_poll(&fake_device, &sub_a, &out), ERROR_NONE); CHECK_EQ(out.key, 'a'); // sub_b was unsubscribed before the poll, so nothing was fanned out to it. CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_TIMEOUT); CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_a), ERROR_NONE); CHECK_EQ(device_stop(&fake_device), ERROR_NONE); CHECK_EQ(device_remove(&fake_device), ERROR_NONE); CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE); }