From 53dd7c8116bf3cbcce049bc0470ae572f34a83a2 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 12 Aug 2026 04:47:08 +1000 Subject: [PATCH] [client] keybind: test app dispatch lifecycle --- client/tests/CMakeLists.txt | 35 +++ client/tests/app_keybind_test.c | 456 ++++++++++++++++++++++++++++++++ 2 files changed, 491 insertions(+) create mode 100644 client/tests/app_keybind_test.c diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index a42c19bc..0a6c8c43 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -355,6 +355,41 @@ foreach(name IN LISTS KEYBIND_CASES) ) endforeach() +add_executable(app-keybind-tests + app_keybind_test.c + ../src/app.c +) +target_compile_definitions(app-keybind-tests PRIVATE + CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 +) +target_include_directories(app-keybind-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${PROJECT_TOP}/repos/gui/cimgui" + "${PROJECT_TOP}/repos/gui/cimgui/imgui" +) +target_link_libraries(app-keybind-tests + ${EXE_FLAGS} + lg_common +) +set(APP_KEYBIND_CASES + invalid + null-callback + duplicate + normal + dispatch + release-all + focus + reentrant +) +foreach(name IN LISTS APP_KEYBIND_CASES) + add_test(NAME app-keybind-${name} + COMMAND app-keybind-tests ${name} + ) + set_tests_properties(app-keybind-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + add_executable(clipboard-tests clipboard_test.c ../src/clipboard.c diff --git a/client/tests/app_keybind_test.c b/client/tests/app_keybind_test.c new file mode 100644 index 00000000..6893896c --- /dev/null +++ b/client/tests/app_keybind_test.c @@ -0,0 +1,456 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "app.h" +#include "app_internal.h" +#include "main.h" +#include "test.h" + +#include "common/array.h" +#include "common/ll.h" + +#include +#include +#include +#include +#include + +#define MAX_KEY 32U + +struct KeyEvent +{ + int key; + bool down; +}; + +struct Trace +{ + struct KeyEvent key[MAX_KEY]; + unsigned int keyN; + unsigned int cbN; + unsigned int releaseN; + unsigned int grabN; + unsigned int quietN; + unsigned int cursorN; + unsigned int resetN; + unsigned int pointerN; + unsigned int minimizeN; + unsigned int realignN; + int cbKey; + bool input; + bool available; + bool exclusive; + bool fullscreen; + bool grab; + bool quiet; + bool cursor; + LG_DSPointer pointer; +}; + +struct Reentrant +{ + KeybindHandle handle; + unsigned int oldN; + unsigned int newN; +}; + +static struct Trace t; +static struct Reentrant re; +static struct LG_DisplayServerOps ds; +static ImGuiIO io; + +struct AppState g_state; +struct CursorState g_cursor; +struct AppParams g_params; +_Atomic(enum RunState) p_appState; + +const char * linux_to_display[KEY_MAX] = +{ + [KEY_ESC] = "Esc", + [KEY_A] = "A", + [KEY_B] = "B", + [KEY_C] = "C", +}; + +const int linux_to_imgui[KEY_MAX] = { 0 }; + +static bool getFullscreen(void) +{ + return t.fullscreen; +} + +static void minimize(void) +{ + ++t.minimizeN; +} + +static void setPointer(LG_DSPointer pointer) +{ + ++t.pointerN; + t.pointer = pointer; +} + +static void realign(void) +{ + ++t.realignN; +} + +static void init(void) +{ + memset(&t, 0, sizeof(t)); + memset(&re, 0, sizeof(re)); + memset(&g_state, 0, sizeof(g_state)); + memset(&g_cursor, 0, sizeof(g_cursor)); + memset(&g_params, 0, sizeof(g_params)); + memset(&ds, 0, sizeof(ds)); + memset(&io, 0, sizeof(io)); + + t.input = true; + t.available = true; + + ds.getFullscreen = getFullscreen; + ds.minimize = minimize; + ds.setPointer = setPointer; + ds.realignPointer = realign; + + g_state.bindings = ll_new(); + g_state.overlays = ll_new(); + g_state.io = &io; + g_state.ds = &ds; + g_state.focused = true; + + g_params.escapeKey = KEY_ESC; + atomic_store(&p_appState, APP_STATE_RUNNING); + + CHECK(g_state.bindings); + CHECK(g_state.overlays); +} + +static void fini(void) +{ + app_releaseAllKeybinds(); + ll_free(g_state.bindings); + ll_free(g_state.overlays); +} + +static bool addKey(bool down, int key) +{ + CHECK(t.keyN < MAX_KEY); + t.key[t.keyN].key = key; + t.key[t.keyN++].down = down; + return true; +} + +bool core_inputEnabled(void) +{ + return t.input; +} + +void core_setGrab(bool enable) +{ + ++t.grabN; + t.grab = enable; +} + +void core_setGrabQuiet(bool enable) +{ + ++t.quietN; + t.quiet = enable; +} + +void core_setCursorInView(bool enable) +{ + ++t.cursorN; + t.cursor = enable; +} + +void core_resetOverlayInputState(void) +{ + ++t.resetN; +} + +void core_updateOverlayState(void) +{ +} + +bool evdev_isExclusive(void) +{ + return t.exclusive; +} + +bool lgInput_available(void) +{ + return t.available; +} + +bool lgInput_keyDown(int key) +{ + return addKey(true, key); +} + +bool lgInput_keyUp(int key) +{ + return addKey(false, key); +} + +void lgInput_releaseKeys(void) +{ + ++t.releaseN; +} + +void ImGuiIO_AddKeyEvent(ImGuiIO * self, ImGuiKey key, bool down) +{ +} + +static void count(int key, void * opaque) +{ + ++t.cbN; + t.cbKey = key; + CHECK(opaque == &t); +} + +static void next(int key, void * opaque) +{ + struct Reentrant * state = opaque; + ++state->newN; +} + +static void replace(int key, void * opaque) +{ + struct Reentrant * state = opaque; + ++state->oldN; + app_releaseKeybind(&state->handle); + CHECK(!state->handle); + state->handle = app_registerKeybind(key, next, state, "new"); + CHECK(state->handle); +} + +static void checkKey(unsigned int index, bool down, int key) +{ + CHECK(index < t.keyN); + CHECK(t.key[index].down == down); + CHECK(t.key[index].key == key); +} + +static void testInvalid(void) +{ + init(); + CHECK(!app_registerKeybind(KEY_RESERVED, count, &t, "reserved")); + CHECK(!app_registerKeybind(KEY_MAX, count, &t, "past-end")); + CHECK(!app_registerKeybind(KEY_MAX - 1, count, &t, "unmapped")); + CHECK(ll_count(g_state.bindings) == 0); + + KeybindHandle handle = app_registerKeybind(KEY_A, count, &t, "valid"); + CHECK(handle); + CHECK(ll_count(g_state.bindings) == 1); + fini(); +} + +static void testNullCallback(void) +{ + init(); + CHECK(!app_registerKeybind(KEY_A, NULL, NULL, "null callback")); + CHECK(ll_count(g_state.bindings) == 0); + fini(); +} + +static void testDuplicate(void) +{ + init(); + KeybindHandle first = app_registerKeybind(KEY_A, count, &t, "first"); + CHECK(first); + CHECK(!app_registerKeybind(KEY_A, count, &t, "duplicate")); + CHECK(ll_count(g_state.bindings) == 1); + + app_releaseKeybind(&first); + CHECK(!first); + CHECK(ll_count(g_state.bindings) == 0); + app_releaseKeybind(&first); + CHECK(!first); + + first = app_registerKeybind(KEY_A, count, &t, "replacement"); + CHECK(first); + CHECK(ll_count(g_state.bindings) == 1); + fini(); +} + +static void testNormal(void) +{ + init(); + app_handleKeyPress(KEY_A); + app_handleKeyPress(KEY_A); + app_handleKeyRelease(KEY_A); + CHECK(t.keyN == 3); + checkKey(0, true , KEY_A); + checkKey(1, true , KEY_A); + checkKey(2, false, KEY_A); + + t.exclusive = true; + app_handleKeyPress(KEY_B); + app_handleKeyRelease(KEY_B); + CHECK(t.keyN == 3); + + t.exclusive = false; + g_params.ignoreWindowsKeys = true; + app_handleKeyPress(KEY_LEFTMETA); + app_handleKeyRelease(KEY_LEFTMETA); + CHECK(t.keyN == 3); + fini(); +} + +static void testDispatch(void) +{ + init(); + CHECK(app_registerKeybind(KEY_A, count, &t, "action")); + app_handleKeyPress(KEY_ESC); + CHECK(g_state.escapeActive); + CHECK(g_state.escapeAction == -1); + CHECK(t.keyN == 0); + + app_handleKeyPress(KEY_A); + app_handleKeyPress(KEY_A); + CHECK(t.cbN == 2); + CHECK(t.cbKey == KEY_A); + CHECK(g_state.escapeAction == KEY_A); + CHECK(t.keyN == 0); + + app_handleKeyRelease(KEY_A); + app_handleKeyRelease(KEY_ESC); + CHECK(!g_state.escapeActive); + CHECK(t.cbN == 2); + CHECK(t.keyN == 0); + fini(); +} + +static void testReleaseAll(void) +{ + init(); + CHECK(app_registerKeybind(KEY_A, count, &t, "a")); + CHECK(app_registerKeybind(KEY_B, count, &t, "b")); + CHECK(app_registerKeybind(KEY_C, count, &t, "c")); + CHECK(ll_count(g_state.bindings) == 3); + + app_releaseAllKeybinds(); + CHECK(ll_count(g_state.bindings) == 0); + app_releaseAllKeybinds(); + CHECK(ll_count(g_state.bindings) == 0); + CHECK(app_registerKeybind(KEY_A, count, &t, "again")); + CHECK(ll_count(g_state.bindings) == 1); + fini(); +} + +static void testFocus(void) +{ + init(); + t.fullscreen = true; + g_state.escapeActive = true; + g_cursor.grab = true; + g_cursor.motionValid = true; + g_params.releaseKeysOnFocusLoss = true; + g_params.minimizeOnFocusLoss = true; + g_params.captureOnFocus = true; + + app_handleFocusEvent(false); + CHECK(!g_state.focused); + CHECK(!g_state.escapeActive); + CHECK(!g_cursor.motionValid); + CHECK(g_cursor.realign); + CHECK(t.releaseN == 1); + CHECK(t.quietN == 1); + CHECK(!t.quiet); + CHECK(t.cursorN == 1); + CHECK(!t.cursor); + CHECK(t.pointerN == 1); + CHECK(t.pointer == LG_POINTER_NONE); + CHECK(t.minimizeN == 1); + CHECK(t.realignN == 1); + + app_handleFocusEvent(false); + CHECK(t.releaseN == 1); + CHECK(t.realignN == 1); + + app_handleFocusEvent(true); + CHECK(g_state.focused); + CHECK(t.grabN == 1); + CHECK(t.grab); + CHECK(t.realignN == 2); + + g_params.releaseKeysOnFocusLoss = false; + app_handleFocusEvent(false); + CHECK(t.releaseN == 1); + fini(); +} + +static void testReentrant(void) +{ + init(); + re.handle = app_registerKeybind(KEY_A, replace, &re, "old"); + CHECK(re.handle); + app_handleKeyPress(KEY_ESC); + app_handleKeyPress(KEY_A); + CHECK(re.oldN == 1); + CHECK(re.newN == 0); + CHECK(re.handle); + CHECK(ll_count(g_state.bindings) == 1); + + app_handleKeyPress(KEY_A); + CHECK(re.oldN == 1); + CHECK(re.newN == 1); + CHECK(ll_count(g_state.bindings) == 1); + fini(); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "invalid" , testInvalid }, + { "null-callback", testNullCallback }, + { "duplicate" , testDuplicate }, + { "normal" , testNormal }, + { "dispatch" , testDispatch }, + { "release-all" , testReleaseAll }, + { "focus" , testFocus }, + { "reentrant" , testReentrant }, +}; + +int main(int argc, char ** argv) +{ + if (argc != 2) + { + fprintf(stderr, "usage: %s \n", argv[0]); + return EXIT_FAILURE; + } + + debug_init(); + for (unsigned int i = 0; i < ARRAY_LENGTH(tests); ++i) + if (strcmp(argv[1], tests[i].name) == 0) + { + tests[i].run(); + return 0; + } + + fprintf(stderr, "unknown test: %s\n", argv[1]); + return EXIT_FAILURE; +}