diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 4895581e..c0c2ec79 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -293,6 +293,37 @@ foreach(name IN LISTS INPUT_CASES) ) endforeach() +add_executable(keybind-tests + keybind_test.c +) +target_compile_definitions(keybind-tests PRIVATE + CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 +) +target_include_directories(keybind-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${PROJECT_TOP}/repos/gui/cimgui" + "${PROJECT_TOP}/repos/gui/cimgui/imgui" +) +target_link_libraries(keybind-tests + ${EXE_FLAGS} + lg_common +) +set(KEYBIND_CASES + common + os + order + repeat + actions +) +foreach(name IN LISTS KEYBIND_CASES) + add_test(NAME keybind-${name} + COMMAND keybind-tests ${name} + ) + set_tests_properties(keybind-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + add_executable(clipboard-tests clipboard_test.c ../src/clipboard.c diff --git a/client/tests/keybind_test.c b/client/tests/keybind_test.c new file mode 100644 index 00000000..60e18197 --- /dev/null +++ b/client/tests/keybind_test.c @@ -0,0 +1,474 @@ +/** + * 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 "test.h" + +#include +#include +#include +#include +#include +#include + +#include "common/array.h" + +#include "../src/keybind.c" + +#define MAX_BIND 64U +#define MAX_KEY 64U +#define MAX_OP 128U + +struct Bind +{ + struct KeybindHandle handle; + bool active; +}; + +struct KeyEvent +{ + int key; + bool down; +}; + +struct Trace +{ + struct Bind bind[MAX_BIND]; + struct KeyEvent key[MAX_KEY]; + int op[MAX_OP]; + unsigned int bindN; + unsigned int tryN; + unsigned int activeN; + unsigned int collisionN; + unsigned int releaseN; + unsigned int keyN; + unsigned int opN; + unsigned int posN; + unsigned int cursorN; + unsigned int realignN; + unsigned int alertN; + unsigned int msgN; + unsigned int videoN; + unsigned int overlayN; + bool isLinux; + bool fullscreen; + bool cursorInView; + LGMsg msg; + char alert[64]; +}; + +static struct Trace t; +static struct LG_DisplayServerOps ds; + +struct AppState g_state; +struct CursorState g_cursor; +struct AppParams g_params; +_Atomic(enum RunState) p_appState; + +static const int fKeys[] = +{ + KEY_F1 , KEY_F2 , KEY_F3 , KEY_F4, + KEY_F5 , KEY_F6 , KEY_F7 , KEY_F8, + KEY_F9 , KEY_F10, KEY_F11, KEY_F12, +}; + +static void realign(void) +{ + ++t.realignN; +} + +static void reset(void) +{ + memset(&t, 0, sizeof(t)); + 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)); + ds.realignPointer = realign; + g_state.ds = &ds; + atomic_store(&p_appState, APP_STATE_RUNNING); +} + +static struct Bind * find(int sc) +{ + for (unsigned int i = 0; i < t.bindN; ++i) + if (t.bind[i].active && t.bind[i].handle.sc == sc) + return &t.bind[i]; + return NULL; +} + +static void call(int sc) +{ + struct Bind * bind = find(sc); + CHECK(bind); + CHECK(bind->handle.callback); + bind->handle.callback(sc, bind->handle.opaque); +} + +KeybindHandle app_registerKeybind(int sc, KeybindFn callback, void * opaque, + const char * description) +{ + CHECK(sc > KEY_RESERVED && sc < KEY_MAX); + CHECK(callback); + CHECK(description); + ++t.tryN; + + if (find(sc)) + { + ++t.collisionN; + return NULL; + } + + CHECK(t.bindN < MAX_BIND); + CHECK(t.opN < MAX_OP); + struct Bind * bind = &t.bind[t.bindN++]; + bind->active = true; + bind->handle.sc = sc; + bind->handle.callback = callback; + bind->handle.description = description; + bind->handle.opaque = opaque; + t.op[t.opN++] = sc; + ++t.activeN; + return &bind->handle; +} + +void app_releaseKeybind(KeybindHandle * handle) +{ + CHECK(handle); + if (!*handle) + return; + + struct Bind * bind = NULL; + for (unsigned int i = 0; i < t.bindN; ++i) + if (&t.bind[i].handle == *handle) + { + bind = &t.bind[i]; + break; + } + + CHECK(bind); + CHECK(bind->active); + CHECK(t.opN < MAX_OP); + bind->active = false; + t.op[t.opN++] = -bind->handle.sc; + --t.activeN; + ++t.releaseN; + *handle = NULL; +} + +bool app_guestIsLinux(void) +{ + return t.isLinux; +} + +void app_setFullscreen(bool fullscreen) +{ + t.fullscreen = fullscreen; +} + +bool app_getFullscreen(void) +{ + return t.fullscreen; +} + +void app_stopVideo(bool stop) +{ + ++t.videoN; + g_state.stopVideo = stop; +} + +void app_setOverlay(bool enable) +{ + ++t.overlayN; + g_state.overlayInput = enable; +} + +void app_alert(LG_MsgAlert type, const char * format, ...) +{ + CHECK(type == LG_ALERT_INFO); + va_list args; + va_start(args, format); + vsnprintf(t.alert, sizeof(t.alert), format, args); + va_end(args); + ++t.alertN; +} + +void core_updatePositionInfo(void) +{ + ++t.posN; +} + +void core_setCursorInView(bool enable) +{ + ++t.cursorN; + t.cursorInView = enable; +} + +static bool key(bool down, int sc) +{ + CHECK(t.keyN < MAX_KEY); + t.key[t.keyN].key = sc; + t.key[t.keyN++].down = down; + return true; +} + +bool lgInput_keyDown(int sc) +{ + return key(true, sc); +} + +bool lgInput_keyUp(int sc) +{ + return key(false, sc); +} + +void lgMessage_post(const LGMsg * msg) +{ + CHECK(msg); + t.msg = *msg; + ++t.msgN; +} + +static void nop(int sc, void * opaque) +{ +} + +static void checkBind(unsigned int index, int sc, const char * description) +{ + CHECK(index < t.bindN); + CHECK(t.bind[index].active); + CHECK(t.bind[index].handle.sc == sc); + CHECK(strcmp(t.bind[index].handle.description, description) == 0); +} + +static void checkKey(unsigned int index, bool down, int sc) +{ + CHECK(index < t.keyN); + CHECK(t.key[index].down == down); + CHECK(t.key[index].key == sc); +} + +static void testCommon(void) +{ + reset(); + keybind_commonRegister(); + + CHECK(t.tryN == 6); + CHECK(t.activeN == 6); + CHECK(t.collisionN == 0); + checkBind(0, KEY_F, "Full screen toggle"); + checkBind(1, KEY_V, "Video stream toggle"); + checkBind(2, KEY_R, "Rotate the output clockwise by 90° increments"); + checkBind(3, KEY_Q, "Quit"); + checkBind(4, KEY_O, "Toggle overlay"); + checkBind(5, KEY_EQUAL, + "Set guest resolution to match window size"); + + keybind_commonRegister(); + CHECK(t.tryN == 12); + CHECK(t.activeN == 6); + CHECK(t.collisionN == 6); +} + +static void testOS(void) +{ + reset(); + CHECK(app_registerKeybind(KEY_F1, nop, NULL, "external")); + t.isLinux = true; + keybind_inputRegister(); + + CHECK(t.tryN == 21); + CHECK(t.activeN == 20); + CHECK(t.collisionN == 1); + CHECK(find(KEY_I)); + CHECK(find(KEY_LEFTMETA)); + CHECK(find(KEY_RIGHTMETA)); + for (unsigned int i = 0; i < ARRAY_LENGTH(fKeys); ++i) + CHECK(find(fKeys[i])); + + struct Bind * fixed = find(KEY_I); + struct Bind * oldF2 = find(KEY_F2); + const unsigned int op = t.opN; + keybind_inputRegister(); + CHECK(t.activeN == 20); + CHECK(t.releaseN == 11); + CHECK(t.collisionN == 2); + CHECK(find(KEY_I) == fixed); + CHECK(find(KEY_F2) != oldF2); + CHECK(!oldF2->active); + CHECK(t.opN == op + 22); + for (unsigned int i = 0; i < 11; ++i) + { + CHECK(t.op[op + i] < 0); + CHECK(t.op[op + 11 + i] > 0); + } + + t.isLinux = false; + keybind_inputRegister(); + CHECK(t.activeN == 9); + CHECK(t.releaseN == 22); + CHECK(find(KEY_I) == fixed); + CHECK(strcmp(find(KEY_F1)->handle.description, "external") == 0); + for (unsigned int i = 1; i < ARRAY_LENGTH(fKeys); ++i) + CHECK(!find(fKeys[i])); + + t.isLinux = true; + keybind_inputRegister(); + CHECK(t.activeN == 20); + CHECK(t.collisionN == 3); +} + +static void testOrder(void) +{ + reset(); + t.isLinux = true; + keybind_inputRegister(); + call(KEY_F5); + + CHECK(t.keyN == 6); + checkKey(0, true , KEY_LEFTCTRL); + checkKey(1, true , KEY_LEFTALT ); + checkKey(2, true , KEY_F5 ); + checkKey(3, false, KEY_F5 ); + checkKey(4, false, KEY_LEFTALT ); + checkKey(5, false, KEY_LEFTCTRL); +} + +static void testRepeat(void) +{ + reset(); + keybind_inputRegister(); + call(KEY_LEFTMETA); + call(KEY_LEFTMETA); + call(KEY_UP); + call(KEY_UP); + + CHECK(t.keyN == 8); + checkKey(0, true , KEY_LEFTMETA); + checkKey(1, false, KEY_LEFTMETA); + checkKey(2, true , KEY_LEFTMETA); + checkKey(3, false, KEY_LEFTMETA); + checkKey(4, true , KEY_VOLUMEUP); + checkKey(5, false, KEY_VOLUMEUP); + checkKey(6, true , KEY_VOLUMEUP); + checkKey(7, false, KEY_VOLUMEUP); +} + +static void testActions(void) +{ + reset(); + keybind_commonRegister(); + keybind_inputRegister(); + + call(KEY_F); + CHECK(t.fullscreen); + call(KEY_F); + CHECK(!t.fullscreen); + + call(KEY_V); + CHECK(g_state.stopVideo); + call(KEY_V); + CHECK(!g_state.stopVideo); + CHECK(t.videoN == 2); + + g_params.winRotate = LG_ROTATE_MAX - 1; + call(KEY_R); + CHECK(g_params.winRotate == 0); + call(KEY_R); + CHECK(g_params.winRotate == 1); + CHECK(t.posN == 2); + + call(KEY_O); + CHECK(g_state.overlayInput); + call(KEY_O); + CHECK(!g_state.overlayInput); + CHECK(t.overlayN == 2); + + g_state.ignoreInput = false; + call(KEY_I); + CHECK(g_state.ignoreInput); + CHECK(t.cursorN == 1); + CHECK(!t.cursorInView); + CHECK(strcmp(t.alert, "Input Disabled") == 0); + call(KEY_I); + CHECK(!g_state.ignoreInput); + CHECK(t.realignN == 1); + CHECK(strcmp(t.alert, "Input Enabled") == 0); + + g_cursor.sens = 8; + call(KEY_INSERT); + call(KEY_INSERT); + CHECK(g_cursor.sens == 9); + CHECK(strcmp(t.alert, "Sensitivity: +9") == 0); + g_cursor.sens = -8; + call(KEY_DELETE); + call(KEY_DELETE); + CHECK(g_cursor.sens == -9); + CHECK(strcmp(t.alert, "Sensitivity: -9") == 0); + + g_state.windowW = 1920; + g_state.windowH = 1080; + call(KEY_EQUAL); + CHECK(t.msgN == 0); + CHECK(strcmp(t.alert, + "The guest doesn't support this feature") == 0); + g_state.transportFeatures = LG_TRANSPORT_FEATURE_WINDOW_SIZE; + call(KEY_EQUAL); + CHECK(t.msgN == 1); + CHECK(t.msg.type == LG_MSG_WINDOWSIZE); + CHECK(t.msg.windowSize.width == 1920); + CHECK(t.msg.windowSize.height == 1080); + + call(KEY_Q); + CHECK(app_getState() == APP_STATE_SHUTDOWN); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "common" , testCommon }, + { "os" , testOS }, + { "order" , testOrder }, + { "repeat" , testRepeat }, + { "actions", testActions }, +}; + +int main(int argc, char ** argv) +{ + if (argc != 2) + { + fprintf(stderr, "usage: %s \n", argv[0]); + return EXIT_FAILURE; + } + + 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; +}