diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 555f507f..6c80061a 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -366,6 +366,44 @@ foreach(name IN LISTS FRAME_SCHEDULER_CASES) ) endforeach() +add_executable(config-message-tests + config_message_test.c + ../src/message.c + ../src/core.c + ../src/config.c +) +target_compile_definitions(config-message-tests PRIVATE + CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 +) +target_include_directories(config-message-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${PROJECT_TOP}/repos/gui/cimgui" + "${PROJECT_TOP}/repos/gui/cimgui/imgui" +) +target_link_libraries(config-message-tests + ${EXE_FLAGS} + lg_common +) +set(CONFIG_MESSAGE_CASES + debounce + latest + equal + reinit + control + cfg-values + cfg-position + cfg-size + cfg-precedence +) +foreach(name IN LISTS CONFIG_MESSAGE_CASES) + add_test(NAME config-message-${name} + COMMAND config-message-tests ${name} + ) + set_tests_properties(config-message-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + if(ENABLE_AUDIO) pkg_check_modules(AUDIO_TEST_SAMPLERATE REQUIRED IMPORTED_TARGET samplerate) diff --git a/client/tests/config_message_test.c b/client/tests/config_message_test.c new file mode 100644 index 00000000..a09f3bc3 --- /dev/null +++ b/client/tests/config_message_test.c @@ -0,0 +1,473 @@ +/** + * 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 "config.h" +#include "kb.h" +#include "main.h" +#include "message.h" +#include "test.h" + +#include "common/debug.h" +#include "common/option.h" + +#include +#include +#include +#include +#include +#include + +#define MAX_CTL 16U +#define DEBOUNCE_US UINT64_C(500000) +#define TEST_ALARM_S 5U + +struct LG_Transport +{ + int unused; +}; + +struct Trace +{ + uint64_t now; + unsigned int send; + LG_TransportStatus status; + LG_TransportControl ctl[MAX_CTL]; + LG_TransportControlToken token[MAX_CTL]; +}; + +static struct Trace t; +static struct LG_Transport tr; + +struct AppState g_state; +struct CursorState g_cursor; +struct AppParams g_params; + +const char * linux_to_str[KEY_MAX] = +{ + [KEY_A] = "KEY_A", + [KEY_RIGHTCTRL] = "KEY_RIGHTCTRL", +}; + +static const char * renAName(void) +{ + return "EGL"; +} + +static const char * renBName(void) +{ + return "OpenGL"; +} + +static LG_RendererOps renA = +{ + .getName = renAName, +}; + +static LG_RendererOps renB = +{ + .getName = renBName, +}; + +LG_RendererOps * LG_Renderers[] = +{ + &renA, + &renB, + NULL, +}; + +bool lgTransport_isValid(const char * name) +{ + return name && + (strcmp(name, "lgmp") == 0 || strcmp(name, "spice") == 0); +} + +int clock_gettime(clockid_t id, struct timespec * ts) +{ + const uint64_t ns = t.now * UINT64_C(1000); + ts->tv_sec = ns / UINT64_C(1000000000); + ts->tv_nsec = ns % UINT64_C(1000000000); + return 0; +} + +static LG_TransportStatus sendCtl(LG_Transport * transport, + const LG_TransportControl * control, LG_TransportControlToken * token) +{ + CHECK(transport == &tr); + CHECK(control); + CHECK(token); + CHECK(t.send < MAX_CTL); + + const unsigned int no = t.send++; + t.ctl[no] = *control; + t.token[no] = no + 1; + *token = t.token[no]; + return t.status; +} + +static const LG_TransportOps trOps = +{ + .name = "fake", + .sendControl = sendCtl, +}; + +static void msgInit(void) +{ + t.now = UINT64_C(1000000); + t.status = LG_TRANSPORT_OK; + g_state.transport.handle = &tr; + g_state.transport.ops = &trOps; + g_state.transportFeatures = LG_TRANSPORT_FEATURE_WINDOW_SIZE; + g_state.srcSize = (struct Point) { 1920, 1080 }; + CHECK(lgMessage_init()); +} + +static void post(unsigned int width, unsigned int height) +{ + const LGMsg msg = + { + .type = LG_MSG_WINDOWSIZE, + .windowSize = + { + .width = width, + .height = height, + }, + }; + lgMessage_post(&msg); +} + +static void step(uint64_t us) +{ + t.now += us; +} + +static void mature(void) +{ + step(DEBOUNCE_US); + lgMessage_process(); +} + +static void checkCtl(unsigned int no, unsigned int width, + unsigned int height) +{ + CHECK(no < t.send); + CHECK(t.ctl[no].type == LG_TRANSPORT_CONTROL_WINDOW_SIZE); + CHECK(t.ctl[no].windowSize.width == width); + CHECK(t.ctl[no].windowSize.height == height); + CHECK(t.token[no] == no + 1); +} + +static void testDebounce(void) +{ + msgInit(); + post(800, 600); + lgMessage_process(); + CHECK(t.send == 0); + step(DEBOUNCE_US - 1); + lgMessage_process(); + CHECK(t.send == 0); + step(1); + lgMessage_process(); + CHECK(t.send == 1); + checkCtl(0, 800, 600); + + post(800, 600); + mature(); + CHECK(t.send == 1); + post(801, 600); + mature(); + CHECK(t.send == 2); + checkCtl(1, 801, 600); + lgMessage_deinit(); +} + +static void testLatest(void) +{ + msgInit(); + post(800, 600); + step(100); + post(1024, 768); + lgMessage_process(); + CHECK(t.send == 0); + + step(DEBOUNCE_US - 1); + lgMessage_process(); + CHECK(t.send == 0); + step(1); + lgMessage_process(); + CHECK(t.send == 1); + checkCtl(0, 1024, 768); + lgMessage_deinit(); +} + +static void testEqual(void) +{ + msgInit(); + post(800, 600); + post(1024, 768); + mature(); + + CHECK(t.send == 1); + /* Posting order decides when timestamps are equal. */ + checkCtl(0, 1024, 768); + lgMessage_deinit(); +} + +static void testReinit(void) +{ + msgInit(); + post(640, 480); + lgMessage_deinit(); + CHECK(lgMessage_init()); + mature(); + CHECK(t.send == 0); + + post(640, 480); + mature(); + CHECK(t.send == 1); + lgMessage_deinit(); + CHECK(lgMessage_init()); + post(640, 480); + mature(); + + /* A new message instance must not inherit its old duplicate filter. */ + CHECK(t.send == 2); + checkCtl(1, 640, 480); + lgMessage_deinit(); +} + +static void testControl(void) +{ + msgInit(); + g_state.transportFeatures = 0; + post(100, 100); + mature(); + CHECK(t.send == 0); + + g_state.transportFeatures = LG_TRANSPORT_FEATURE_WINDOW_SIZE; + g_state.transport.handle = NULL; + post(200, 200); + mature(); + CHECK(t.send == 0); + + g_state.transport.handle = &tr; + g_state.srcSize = (struct Point) { 300, 300 }; + post(300, 300); + mature(); + CHECK(t.send == 0); + + t.status = LG_TRANSPORT_ERROR; + post(400, 400); + mature(); + CHECK(t.send == 1); + checkCtl(0, 400, 400); + step(DEBOUNCE_US); + lgMessage_process(); + CHECK(t.send == 1); + + t.status = LG_TRANSPORT_UNAVAILABLE; + post(500, 500); + mature(); + CHECK(t.send == 2); + checkCtl(1, 500, 500); + + t.status = LG_TRANSPORT_OK; + post(600, 600); + mature(); + CHECK(t.send == 3); + checkCtl(2, 600, 600); + lgMessage_deinit(); +} + +static struct Option * opt(const char * module, const char * name) +{ + struct Option * result = option_get(module, name); + CHECK(result); + return result; +} + +static void checkStr(char * value, const char * expected) +{ + CHECK(value); + CHECK(strcmp(value, expected) == 0); + free(value); +} + +static void testCfgValues(void) +{ + config_init(); + + struct Option * pos = opt("win", "position"); + CHECK(pos->parser(pos, "center")); + CHECK(g_params.center); + checkStr(pos->toString(pos), "center"); + CHECK(pos->parser(pos, "-10x20")); + CHECK(!g_params.center); + CHECK(g_params.x == -10); + CHECK(g_params.y == 20); + checkStr(pos->toString(pos), "-10x20"); + + struct Option * size = opt("win", "size"); + CHECK(size->parser(size, "1920x1080")); + CHECK(g_params.w == 1920); + CHECK(g_params.h == 1080); + checkStr(size->toString(size), "1920x1080"); + + struct Option * mic = opt("audio", "micDefault"); + CHECK(mic->parser(mic, "ALLOW")); + CHECK(g_params.micDefaultState == MIC_DEFAULT_ALLOW); + checkStr(mic->toString(mic), "allow"); + CHECK(!mic->parser(mic, "sometimes")); + + struct Option * resampler = opt("audio", "resampler"); + CHECK(resampler->parser(resampler, "BACKEND")); + CHECK(g_params.audioResampler == AUDIO_RESAMPLER_BACKEND); + checkStr(resampler->toString(resampler), "backend"); + CHECK(!resampler->parser(resampler, "linear")); + + struct Option * renderer = opt("app", "renderer"); + CHECK(renderer->parser(renderer, "OpenGL")); + CHECK(g_params.forceRenderer); + CHECK(g_params.forceRendererIndex == 1); + checkStr(renderer->toString(renderer), "OpenGL"); + CHECK(renderer->parser(renderer, "auto")); + CHECK(!g_params.forceRenderer); + checkStr(renderer->toString(renderer), "auto"); + + struct Option * key = opt("input", "escapeKey"); + CHECK(key->parser(key, "KEY_A")); + CHECK(key->value.x_int == KEY_A); + char * keyName = key->toString(key); + CHECK(keyName); + CHECK(strstr(keyName, "KEY_A")); + free(keyName); + + struct Option * rotate = opt("win", "rotate"); + const char * error = NULL; + rotate->value.x_int = 270; + CHECK(rotate->validator(rotate, &error)); + rotate->value.x_int = 45; + CHECK(!rotate->validator(rotate, &error)); + CHECK(error); + + config_free(); +} + +static void testCfgPos(void) +{ + config_init(); + struct Option * pos = opt("win", "position"); + CHECK(!pos->parser(pos, NULL)); + CHECK(!pos->parser(pos, "10")); + CHECK(!pos->parser(pos, "10x20tail")); + config_free(); +} + +static void testCfgSize(void) +{ + config_init(); + struct Option * size = opt("win", "size"); + CHECK(!size->parser(size, NULL)); + CHECK(!size->parser(size, "0x20")); + CHECK(!size->parser(size, "-1x20")); + CHECK(!size->parser(size, "10x20tail")); + config_free(); +} + +static void loadCfg(const char * text) +{ + char path[] = "/tmp/lg-config-test-XXXXXX"; + const int fd = mkstemp(path); + CHECK(fd >= 0); + const size_t size = strlen(text); + CHECK(write(fd, text, size) == (ssize_t)size); + CHECK(close(fd) == 0); + CHECK(option_load(path)); + CHECK(unlink(path) == 0); +} + +static void testCfgPrecedence(void) +{ + config_init(); + loadCfg("[win]\nsize=800x600\nposition=10x20\n"); + CHECK(g_params.w == 800); + CHECK(g_params.h == 600); + CHECK(g_params.x == 10); + CHECK(g_params.y == 20); + + char * argv[] = + { + "test", + "win:size=1024x768", + "win:position=center", + }; + CHECK(option_parse(3, argv)); + CHECK(g_params.w == 1024); + CHECK(g_params.h == 768); + CHECK(g_params.center); + + loadCfg("[win]\nsize=1280x720\nposition=30x40\n"); + CHECK(g_params.w == 1280); + CHECK(g_params.h == 720); + CHECK(g_params.x == 30); + CHECK(g_params.y == 40); + CHECK(!g_params.center); + CHECK(option_validate()); + config_free(); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "debounce" , testDebounce }, + { "latest" , testLatest }, + { "equal" , testEqual }, + { "reinit" , testReinit }, + { "control" , testControl }, + { "cfg-values" , testCfgValues }, + { "cfg-position" , testCfgPos }, + { "cfg-size" , testCfgSize }, + { "cfg-precedence", testCfgPrecedence }, +}; + +int main(int argc, char ** argv) +{ + if (argc != 2) + { + fprintf(stderr, "usage: %s \n", argv[0]); + return EXIT_FAILURE; + } + + alarm(TEST_ALARM_S); + debug_init(); + for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++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; +}