[client] keybind: test built-in actions and ordering

This commit is contained in:
Geoffrey McRae
2026-08-12 04:43:19 +10:00
parent fc7c8e9cae
commit eb8b46eac9
2 changed files with 505 additions and 0 deletions

View File

@@ -293,6 +293,37 @@ foreach(name IN LISTS INPUT_CASES)
) )
endforeach() 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 add_executable(clipboard-tests
clipboard_test.c clipboard_test.c
../src/clipboard.c ../src/clipboard.c

474
client/tests/keybind_test.c Normal file
View File

@@ -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 <stdarg.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#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 <case>\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;
}