diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3863238..0218db60 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -237,7 +237,7 @@ jobs: cd doc make dirhtml SPHINXOPTS='-b spelling -W' -j$(nproc) - render-tests: + client-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -259,24 +259,24 @@ jobs: - name: Configure run: | - cmake -S client -B client/build-render-tests \ + cmake -S client -B client/build-client-tests \ -DOPTIMIZE_FOR_NATIVE=OFF \ -DENABLE_LIBDECOR=OFF \ -DENABLE_TEST_TRANSPORT=ON \ -DENABLE_RENDER_TESTS=ON - name: Build - run: cmake --build client/build-render-tests --parallel + run: cmake --build client/build-client-tests --parallel - - name: Run rendering matrix + - name: Run client tests run: | - ctest --test-dir client/build-render-tests \ - --output-on-failure \ - -R looking-glass-render-tests + ctest --test-dir client/build-client-tests - - name: Upload failed captures + - name: Upload client test failures if: failure() uses: actions/upload-artifact@v6 with: - name: render-test-failures - path: /tmp/lg-render-case.* + name: client-test-failures + path: | + client/build-client-tests/Testing/ + /tmp/lg-render-case.* diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 14837ebd..8130c510 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -44,6 +44,10 @@ endif() option(ENABLE_RENDER_TESTS "Build framebuffer rendering integration tests" OFF) add_feature_info(ENABLE_RENDER_TESTS ENABLE_RENDER_TESTS "Pixel-accurate EGL rendering tests using the test transport.") +option(ENABLE_TRANSPORT_TESTS "Build transport integration tests" + ${ENABLE_RENDER_TESTS}) +add_feature_info(ENABLE_TRANSPORT_TESTS ENABLE_TRANSPORT_TESTS + "Transport lifecycle and protocol integration tests.") if(ENABLE_RENDER_TESTS AND NOT ENABLE_TEST_TRANSPORT) message(FATAL_ERROR "ENABLE_RENDER_TESTS requires ENABLE_TEST_TRANSPORT") endif() @@ -244,7 +248,7 @@ target_link_libraries(looking-glass-client cimgui ) -if(ENABLE_RENDER_TESTS) +if(ENABLE_RENDER_TESTS OR ENABLE_TRANSPORT_TESTS) include(CTest) enable_testing() add_subdirectory(tests) diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 0e7322ca..c7e07bc6 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -1,43 +1,63 @@ cmake_minimum_required(VERSION 3.10) -find_package(GTest REQUIRED) -find_program(WESTON_EXECUTABLE NAMES weston) -if(NOT WESTON_EXECUTABLE) - message(FATAL_ERROR "weston is required to run framebuffer rendering tests") +if(ENABLE_TRANSPORT_TESTS) + add_executable(lgmp-transport-tests + lgmp_transport_test.c + ) + target_link_libraries(lgmp-transport-tests + ${EXE_FLAGS} + transport_LGMP + lg_common + lgmp + ) + add_test(NAME lgmp-transport-tests + COMMAND lgmp-transport-tests + ) + set_tests_properties(lgmp-transport-tests PROPERTIES + TIMEOUT 10 + ) endif() -add_executable(looking-glass-render-tests - render_test.cpp -) -add_dependencies(looking-glass-render-tests looking-glass-client) +if(ENABLE_RENDER_TESTS) + find_package(GTest REQUIRED) + find_program(WESTON_EXECUTABLE NAMES weston) + if(NOT WESTON_EXECUTABLE) + message(FATAL_ERROR "weston is required to run framebuffer rendering tests") + endif() -set_target_properties(looking-glass-render-tests PROPERTIES - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED ON -) + add_executable(render-tests + render_test.cpp + ) + add_dependencies(render-tests looking-glass-client) -target_compile_definitions(looking-glass-render-tests PRIVATE - LG_CLIENT_PATH="$" -) + set_target_properties(render-tests PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + ) -target_include_directories(looking-glass-render-tests PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/../include" - "${PROJECT_TOP}/common/include" -) + target_compile_definitions(render-tests PRIVATE + LG_CLIENT_PATH="$" + ) -if(TARGET GTest::gtest_main) - target_link_libraries(looking-glass-render-tests GTest::gtest_main) -else() - target_link_libraries(looking-glass-render-tests GTest::Main) + target_include_directories(render-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../include" + "${PROJECT_TOP}/common/include" + ) + + if(TARGET GTest::gtest_main) + target_link_libraries(render-tests GTest::gtest_main) + else() + target_link_libraries(render-tests GTest::Main) + endif() + + add_test(NAME render-tests + COMMAND /bin/bash + "${CMAKE_CURRENT_SOURCE_DIR}/run-with-weston.sh" + "${WESTON_EXECUTABLE}" + "$" + ) + + set_tests_properties(render-tests PROPERTIES + TIMEOUT 120 + ) endif() - -add_test(NAME looking-glass-render-tests - COMMAND /bin/bash - "${CMAKE_CURRENT_SOURCE_DIR}/run-with-weston.sh" - "${WESTON_EXECUTABLE}" - "$" -) - -set_tests_properties(looking-glass-render-tests PROPERTIES - TIMEOUT 120 -) diff --git a/client/tests/README.md b/client/tests/README.md index 5bcbde2b..1a581c28 100644 --- a/client/tests/README.md +++ b/client/tests/README.md @@ -26,7 +26,7 @@ cmake -S client -B client/build \ -DENABLE_RENDER_TESTS=ON cmake --build client/build ctest --test-dir client/build --output-on-failure \ - -R looking-glass-render-tests + -R render-tests ``` GoogleTest and Weston are required when `ENABLE_RENDER_TESTS` is enabled. @@ -37,7 +37,7 @@ To exercise native HDR on the current Wayland session instead of the headless SDR compositor: ```sh -client/build/tests/looking-glass-render-tests \ +client/build/tests/render-tests \ --gtest_filter='*rgba10*:*rgba16f*' ``` diff --git a/client/tests/lgmp_transport_test.c b/client/tests/lgmp_transport_test.c new file mode 100644 index 00000000..b4a4d456 --- /dev/null +++ b/client/tests/lgmp_transport_test.c @@ -0,0 +1,247 @@ +/** + * 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. + */ + +#include "interface/transport.h" + +#include "common/KVMFR.h" +#include "common/LGMPConfig.h" +#include "common/debug.h" +#include "common/option.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define TEST_SHM_SIZE (2U * 1024U * 1024U) +#define TEST_TIMEOUT 20U +#define WAIT_TIMEOUT (TEST_TIMEOUT + 100U) + +extern const LG_TransportOps LGT_LGMP; + +#define CHECK(x) \ + do \ + { \ + if (!(x)) \ + { \ + fprintf(stderr, "check failed at %s:%d: %s\n", __FILE__, __LINE__, #x); \ + goto cleanup; \ + } \ + } \ + while (0) + +static bool waitForQueuesEmpty(PLGMPHost host, PLGMPHostQueue frameQueue, + PLGMPHostQueue pointerQueue) +{ + for (unsigned i = 0; i < WAIT_TIMEOUT; ++i) + { + if (lgmpHostProcess(host) != LGMP_OK) + return false; + if (lgmpHostQueuePending(frameQueue) == 0 && + lgmpHostQueuePending(pointerQueue) == 0) + return true; + usleep(1000); + } + + return false; +} + +int main(void) +{ + int result = 1; + int fd = -1; + char path[] = "/tmp/lgmp-transport-test-XXXXXX"; + bool pathExists = false; + void * hostMemory = MAP_FAILED; + PLGMPHost host = NULL; + PLGMPHostQueue frameQueue = NULL; + PLGMPHostQueue pointerQueue = NULL; + PLGMPMemory frameMemory = NULL; + PLGMPMemory pointerMemory = NULL; + LG_Transport * transport = NULL; + + debug_init(); + + fd = mkstemp(path); + CHECK(fd >= 0); + pathExists = true; + CHECK(ftruncate(fd, TEST_SHM_SIZE) == 0); + + hostMemory = mmap(NULL, TEST_SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, + fd, 0); + CHECK(hostMemory != MAP_FAILED); + + KVMFR session = { 0 }; + memcpy(session.magic, KVMFR_MAGIC, sizeof(session.magic)); + session.version = KVMFR_VERSION; + memcpy(session.hostver, "transport-test", sizeof("transport-test")); + + CHECK(lgmpHostInit(hostMemory, TEST_SHM_SIZE, &host, sizeof(session), + (uint8_t *)&session) == LGMP_OK); + + const struct LGMPQueueConfig frameConfig = { + .queueID = LGMP_Q_FRAME, + .numMessages = LGMP_Q_FRAME_LEN, + .subTimeout = TEST_TIMEOUT, + }; + const struct LGMPQueueConfig pointerConfig = { + .queueID = LGMP_Q_POINTER, + .numMessages = LGMP_Q_POINTER_LEN, + .subTimeout = TEST_TIMEOUT, + }; + CHECK(lgmpHostQueueNew(host, frameConfig, &frameQueue) == LGMP_OK); + CHECK(lgmpHostQueueNew(host, pointerConfig, &pointerQueue) == LGMP_OK); + + const uint32_t frameSize = + sizeof(KVMFRFrame) + sizeof(FrameBuffer) + sizeof(uint32_t); + CHECK(lgmpHostMemAlloc(host, frameSize, &frameMemory) == LGMP_OK); + KVMFRFrame * wireFrame = lgmpHostMemPtr(frameMemory); + wireFrame->formatVer = 1; + wireFrame->frameSerial = 1; + wireFrame->type = FRAME_TYPE_BGRA; + wireFrame->screenWidth = 1; + wireFrame->screenHeight = 1; + wireFrame->dataWidth = 1; + wireFrame->dataHeight = 1; + wireFrame->frameWidth = 1; + wireFrame->frameHeight = 1; + wireFrame->rotation = FRAME_ROT_0; + wireFrame->stride = 1; + wireFrame->pitch = sizeof(uint32_t); + wireFrame->offset = sizeof(*wireFrame); + wireFrame->sdrWhiteLevel = KVMFR_SDR_WHITE_LEVEL_DEFAULT; + FrameBuffer * framebuffer = (FrameBuffer *)((uint8_t *)wireFrame + + wireFrame->offset); + atomic_store(&framebuffer->wp, sizeof(uint32_t)); + + CHECK(lgmpHostMemAlloc(host, sizeof(KVMFRCursor), &pointerMemory) == + LGMP_OK); + KVMFRCursor * wirePointer = lgmpHostMemPtr(pointerMemory); + wirePointer->x = 1; + wirePointer->y = 1; + + LGT_LGMP.setup(); + option_set_string("lgmp", "shmDevice", path); + option_set_bool("lgmp", "allowDMA", false); + option_set_int("lgmp", "framePollInterval", 0); + option_set_int("lgmp", "cursorPollInterval", 0); + CHECK(LGT_LGMP.create(&transport)); + + CHECK(unlink(path) == 0); + pathExists = false; + + usleep(300000); + CHECK(lgmpHostProcess(host) == LGMP_OK); + + LG_TransportSession transportSession; + CHECK(LGT_LGMP.connect(transport, &transportSession) == LG_TRANSPORT_OK); + + LG_TransportFrame frame; + LG_TransportPointer pointer; + CHECK(LGT_LGMP.nextFrame(transport, false, &frame) == LG_TRANSPORT_TIMEOUT); + CHECK(LGT_LGMP.nextPointer(transport, &pointer) == LG_TRANSPORT_TIMEOUT); + CHECK(lgmpHostQueueHasSubs(frameQueue)); + CHECK(lgmpHostQueueHasSubs(pointerQueue)); + CHECK(lgmpHostQueueNewSubs(frameQueue) == 1); + CHECK(lgmpHostQueueNewSubs(pointerQueue) == 1); + + CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, + pointerMemory) == LGMP_OK); + CHECK(LGT_LGMP.nextFrame(transport, false, &frame) == LG_TRANSPORT_OK); + LGT_LGMP.releaseFrame(transport, &frame); + CHECK(LGT_LGMP.nextPointer(transport, &pointer) == LG_TRANSPORT_OK); + LGT_LGMP.releasePointer(transport, &pointer); + + /* + * Stop with messages pending. Unsubscribing before the host timeout is the + * lifecycle behavior that was lost when LGMP moved behind the transport API. + */ + CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, + pointerMemory) == LGMP_OK); + CHECK(LGT_LGMP.stopFrame); + CHECK(LGT_LGMP.stopPointer); + LGT_LGMP.stopFrame(transport); + LGT_LGMP.stopPointer(transport); + CHECK(!lgmpHostQueueHasSubs(frameQueue)); + CHECK(!lgmpHostQueueHasSubs(pointerQueue)); + + CHECK(waitForQueuesEmpty(host, frameQueue, pointerQueue)); + CHECK(lgmpHostQueuePending(frameQueue) == 0); + CHECK(lgmpHostQueuePending(pointerQueue) == 0); + + CHECK(LGT_LGMP.nextFrame(transport, false, &frame) == LG_TRANSPORT_TIMEOUT); + CHECK(LGT_LGMP.nextPointer(transport, &pointer) == LG_TRANSPORT_TIMEOUT); + CHECK(lgmpHostQueueNewSubs(frameQueue) == 1); + CHECK(lgmpHostQueueNewSubs(pointerQueue) == 1); + + /* The host resends the same serial after a new subscription. */ + CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, + pointerMemory) == LGMP_OK); + CHECK(LGT_LGMP.nextFrame(transport, false, &frame) == LG_TRANSPORT_OK); + LGT_LGMP.releaseFrame(transport, &frame); + CHECK(LGT_LGMP.nextPointer(transport, &pointer) == LG_TRANSPORT_OK); + LGT_LGMP.releasePointer(transport, &pointer); + + /* + * Also recover when the host has already marked the cached handles bad. + * LGMP leaves a timed-out handle non-NULL when unsubscribe fails. + */ + wireFrame->frameSerial = 2; + CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, + pointerMemory) == LGMP_OK); + CHECK(LGT_LGMP.nextFrame(transport, false, &frame) == LG_TRANSPORT_OK); + CHECK(waitForQueuesEmpty(host, frameQueue, pointerQueue)); + CHECK(lgmpHostQueueHasSubs(frameQueue)); + CHECK(lgmpHostQueueHasSubs(pointerQueue)); + + LGT_LGMP.stopFrame(transport); + LGT_LGMP.stopPointer(transport); + CHECK(LGT_LGMP.nextFrame(transport, false, &frame) == LG_TRANSPORT_TIMEOUT); + CHECK(LGT_LGMP.nextPointer(transport, &pointer) == LG_TRANSPORT_TIMEOUT); + CHECK(lgmpHostQueueNewSubs(frameQueue) == 1); + CHECK(lgmpHostQueueNewSubs(pointerQueue) == 1); + + /* Resetting the transport serial makes the host's resend visible. */ + CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, + pointerMemory) == LGMP_OK); + CHECK(LGT_LGMP.nextFrame(transport, false, &frame) == LG_TRANSPORT_OK); + LGT_LGMP.releaseFrame(transport, &frame); + CHECK(LGT_LGMP.nextPointer(transport, &pointer) == LG_TRANSPORT_OK); + LGT_LGMP.releasePointer(transport, &pointer); + + result = 0; + +cleanup: + if (transport) + LGT_LGMP.destroy(&transport); + option_free(); + lgmpHostMemFree(&pointerMemory); + lgmpHostMemFree(&frameMemory); + lgmpHostFree(&host); + if (hostMemory != MAP_FAILED) + munmap(hostMemory, TEST_SHM_SIZE); + if (fd >= 0) + close(fd); + if (pathExists) + unlink(path); + return result; +}