From c0ea0368c6666e5143ff3442edadc3de213df3af Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 12 Aug 2026 03:31:43 +1000 Subject: [PATCH] [client] transport: test fallback lifecycle --- client/tests/CMakeLists.txt | 33 ++ client/tests/transport_fallback_test.c | 613 +++++++++++++++++++++++++ 2 files changed, 646 insertions(+) create mode 100644 client/tests/transport_fallback_test.c diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index d9b6dfab..2dca6432 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -144,6 +144,39 @@ foreach(name IN LISTS MOUSE_CASES) ) endforeach() +add_executable(transport-fallback-tests + transport_fallback_test.c + ../src/transport_fallback.c +) +target_include_directories(transport-fallback-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" +) +target_link_libraries(transport-fallback-tests + ${EXE_FLAGS} + lg_common +) +if(ENABLE_AUDIO) + target_compile_definitions(transport-fallback-tests PRIVATE + ENABLE_AUDIO + ) +endif() +set(TRANSPORT_FALLBACK_CASES + graceful + dead + mismatch + revoke + retry + cancel +) +foreach(name IN LISTS TRANSPORT_FALLBACK_CASES) + add_test(NAME transport-fallback-${name} + COMMAND transport-fallback-tests ${name} + ) + set_tests_properties(transport-fallback-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + add_executable(render-queue-tests render_queue_test.c ../src/render_queue.c diff --git a/client/tests/transport_fallback_test.c b/client/tests/transport_fallback_test.c new file mode 100644 index 00000000..fa53cda1 --- /dev/null +++ b/client/tests/transport_fallback_test.c @@ -0,0 +1,613 @@ +/** + * 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 "transport_fallback.h" +#include "test.h" + +#include "common/debug.h" + +#include +#include +#include +#include +#include +#include + +#define TEST_WAIT_MS 3000U +#define TEST_ALARM_S 5U + +struct LG_Transport +{ + unsigned int id; +}; + +struct ProviderTrace +{ + atomic_uint set; + atomic_uint clear; + atomic_uint drop; +}; + +struct TransportTrace +{ + atomic_uint make; + atomic_uint free; + atomic_uint conn; + atomic_uint disc; + atomic_uint attach; + atomic_uint detach; + atomic_uint on; + atomic_uint off; + atomic_uint getInput; + atomic_uint getAudio; + atomic_uint getClipboard; + atomic_bool alive; + atomic_bool hold; + atomic_bool release; + atomic_bool cancel; + atomic_bool badProvider; + atomic_uint_fast64_t connTime[4]; + unsigned int failConn; + bool uuidValid; + uint8_t uuid[16]; +}; + +struct EventTrace +{ + atomic_uint conn; + atomic_uint disc; + atomic_uint mismatch; + LG_TransportSession session; + uint8_t primary[16]; + uint8_t fallback[16]; +}; + +static struct TransportTrace t; +static struct EventTrace e; +static struct ProviderTrace pInput; +#ifdef ENABLE_AUDIO +static struct ProviderTrace pAudio; +#endif +static struct ProviderTrace pClipboard; + +static const uint8_t UUID_A[16] = + { 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f }; +static const uint8_t UUID_B[16] = + { 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f }; + +static uint64_t nowNS(void) +{ + struct timespec ts; + CHECK(clock_gettime(CLOCK_MONOTONIC, &ts) == 0); + return (uint64_t)ts.tv_sec * UINT64_C(1000000000) + ts.tv_nsec; +} + +static bool waitCount(const atomic_uint * count, unsigned int value) +{ + for (unsigned int i = 0; i < TEST_WAIT_MS; ++i) + { + if (atomic_load(count) >= value) + return true; + usleep(1000); + } + + return false; +} + +static bool waitReady(const LG_TransportFallback * fallback) +{ + for (unsigned int i = 0; i < TEST_WAIT_MS; ++i) + { + if (lgTransportFallback_ready(fallback)) + return true; + usleep(1000); + } + + return false; +} + +static void setRemote(const uint8_t uuid[16]) +{ + t.uuidValid = true; + memcpy(t.uuid, uuid, sizeof(t.uuid)); +} + +static bool fakeCreate(LG_Transport ** transport) +{ + LG_Transport * result = calloc(1, sizeof(*result)); + if (!result) + return false; + + result->id = atomic_fetch_add(&t.make, 1) + 1; + *transport = result; + return true; +} + +static void fakeDestroy(LG_Transport ** transport) +{ + CHECK(transport); + CHECK(*transport); + atomic_fetch_add(&t.free, 1); + free(*transport); + *transport = NULL; +} + +static LG_TransportStatus fakeConnect(LG_Transport * transport, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * opaque) +{ + CHECK(transport); + CHECK(session); + + const unsigned int no = atomic_load(&t.conn); + if (no < 4) + atomic_store(&t.connTime[no], nowNS()); + atomic_fetch_add(&t.conn, 1); + + while (atomic_load(&t.hold) && !atomic_load(&t.release)) + { + if (cancelled && cancelled(opaque)) + { + atomic_store(&t.cancel, true); + return LG_TRANSPORT_UNAVAILABLE; + } + usleep(1000); + } + + if (no < t.failConn) + return LG_TRANSPORT_UNAVAILABLE; + + *session = (LG_TransportSession) { 0 }; + memcpy(session->version, "fallback-test", sizeof("fallback-test")); + memcpy(session->name, "fallback", sizeof("fallback")); + session->uuidValid = t.uuidValid; + memcpy(session->uuid, t.uuid, sizeof(session->uuid)); + atomic_store(&t.alive, true); + return LG_TRANSPORT_OK; +} + +static void fakeDisconnect(LG_Transport * transport) +{ + CHECK(transport); + atomic_store(&t.alive, false); + atomic_fetch_add(&t.disc, 1); +} + +static bool fakeValid(LG_Transport * transport) +{ + CHECK(transport); + return atomic_load(&t.alive); +} + +static bool fakeAttach(LG_Transport * transport, + const LG_SwSurfaceEventOps * events, void * opaque) +{ + CHECK(transport); + CHECK(events); + atomic_fetch_add(&t.attach, 1); + return true; +} + +static void fakeDetach(LG_Transport * transport) +{ + CHECK(transport); + atomic_fetch_add(&t.detach, 1); +} + +static bool fakeActive(LG_Transport * transport, bool active) +{ + CHECK(transport); + atomic_fetch_add(active ? &t.on : &t.off, 1); + return true; +} + +static const LG_InputOps inputOps = +{ + .name = "fake", +}; + +static const LG_AudioOps audioOps = +{ + .name = "fake", +}; + +static const LG_ClipboardOps clipboardOps = +{ + .name = "fake", +}; + +static const LG_InputOps * fakeGetInput( + LG_Transport * transport, void ** opaque) +{ + atomic_fetch_add(&t.getInput, 1); + *opaque = transport; + return &inputOps; +} + +static const LG_AudioOps * fakeGetAudio( + LG_Transport * transport, void ** opaque) +{ + atomic_fetch_add(&t.getAudio, 1); + *opaque = transport; + return &audioOps; +} + +static const LG_ClipboardOps * fakeGetClipboard( + LG_Transport * transport, void ** opaque) +{ + atomic_fetch_add(&t.getClipboard, 1); + *opaque = transport; + return &clipboardOps; +} + +static const LG_SwSurfaceOps surfaceOps = +{ + .attach = fakeAttach, + .detach = fakeDetach, + .setActive = fakeActive, +}; + +static const LG_VideoOps videoOps = +{ + .name = "fake", + .type = LG_VIDEO_TYPE_SW_SURFACE, + .swSurface = &surfaceOps, +}; + +static const LG_VideoOps * fakeGetVideo(LG_Transport * transport) +{ + CHECK(transport); + return &videoOps; +} + +static const LG_TransportOps transportOps = +{ + .name = "fake", + .create = fakeCreate, + .destroy = fakeDestroy, + .connectCancellable = fakeConnect, + .disconnect = fakeDisconnect, + .sessionValid = fakeValid, + .getVideoOps = fakeGetVideo, + .getInputOps = fakeGetInput, + .getAudioOps = fakeGetAudio, + .getClipboardOps = fakeGetClipboard, +}; + +bool lgTransport_isValid(const char * name) +{ + return name && strcmp(name, transportOps.name) == 0; +} + +bool lgTransport_create(const char * name, LG_TransportInstance * instance) +{ + if (!instance || !lgTransport_isValid(name)) + return false; + + *instance = (LG_TransportInstance) { 0 }; + if (!transportOps.create(&instance->handle)) + return false; + instance->ops = &transportOps; + return true; +} + +void lgTransport_destroy(LG_TransportInstance * instance) +{ + CHECK(instance); + if (instance->ops) + instance->ops->destroy(&instance->handle); + *instance = (LG_TransportInstance) { 0 }; +} + +static void setProvider(struct ProviderTrace * trace, const void * ops, + const void * expected, void * opaque) +{ + if (ops) + { + atomic_fetch_add(&trace->set, 1); + if (ops != expected || !opaque) + atomic_store(&t.badProvider, true); + return; + } + + atomic_fetch_add(&trace->clear, 1); + if (opaque) + atomic_store(&t.badProvider, true); +} + +void lgInput_setFallback(const LG_InputOps * ops, void * opaque) +{ + setProvider(&pInput, ops, &inputOps, opaque); +} + +void lgInput_dropFallback(void) +{ + atomic_fetch_add(&pInput.drop, 1); +} + +#ifdef ENABLE_AUDIO +void lgAudio_setFallback(const LG_AudioOps * ops, void * opaque) +{ + setProvider(&pAudio, ops, &audioOps, opaque); +} + +void lgAudio_dropFallback(void) +{ + atomic_fetch_add(&pAudio.drop, 1); +} +#endif + +void lgClipboard_setFallback(const LG_ClipboardOps * ops, void * opaque) +{ + setProvider(&pClipboard, ops, &clipboardOps, opaque); +} + +void lgClipboard_dropFallback(void) +{ + atomic_fetch_add(&pClipboard.drop, 1); +} + +static void onConnected(void * opaque, + const LG_TransportSession * session) +{ + CHECK(opaque == &e); + e.session = *session; + atomic_fetch_add(&e.conn, 1); +} + +static void onDisconnected(void * opaque) +{ + CHECK(opaque == &e); + atomic_fetch_add(&e.disc, 1); +} + +static void onMismatch(void * opaque, const uint8_t primary[16], + const uint8_t fallback[16]) +{ + CHECK(opaque == &e); + memcpy(e.primary, primary, sizeof(e.primary)); + memcpy(e.fallback, fallback, sizeof(e.fallback)); + atomic_fetch_add(&e.mismatch, 1); +} + +static LG_TransportFallback * start(const uint8_t primary[16]) +{ + static const LG_SwSurfaceEventOps surfaceEvents = { 0 }; + static const LG_TransportFallbackEventOps eventOps = + { + .connected = onConnected, + .disconnected = onDisconnected, + .uuidMismatch = onMismatch, + }; + + LG_TransportFallback * fallback = NULL; + CHECK(lgTransportFallback_start("fake", &surfaceEvents, NULL, + &eventOps, &e, primary, &fallback)); + CHECK(fallback); + return fallback; +} + +static void checkPublished(unsigned int set, unsigned int clear, + unsigned int drop) +{ + CHECK(atomic_load(&t.getInput) == set); + CHECK(atomic_load(&t.getAudio) == set); + CHECK(atomic_load(&t.getClipboard) == set); + CHECK(atomic_load(&pInput.set) == set); + CHECK(atomic_load(&pInput.clear) == clear); + CHECK(atomic_load(&pInput.drop) == drop); +#ifdef ENABLE_AUDIO + CHECK(atomic_load(&pAudio.set) == set); + CHECK(atomic_load(&pAudio.clear) == clear); + CHECK(atomic_load(&pAudio.drop) == drop); +#endif + CHECK(atomic_load(&pClipboard.set) == set); + CHECK(atomic_load(&pClipboard.clear) == clear); + CHECK(atomic_load(&pClipboard.drop) == drop); + CHECK(!atomic_load(&t.badProvider)); +} + +static void testGraceful(void) +{ + setRemote(UUID_A); + atomic_store(&t.hold, true); + + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&t.conn, 1)); + lgTransportFallback_requestVideoActive(fallback, true); + CHECK(lgTransportFallback_videoRequested(fallback)); + atomic_store(&t.release, true); + CHECK(waitReady(fallback)); + CHECK(waitCount(&e.conn, 1)); + + CHECK(lgTransportFallback_admitted(fallback)); + CHECK(atomic_load(&t.on) == 1); + CHECK(memcmp(e.session.uuid, UUID_A, sizeof(UUID_A)) == 0); + checkPublished(1, 0, 0); + + lgTransportFallback_stop(&fallback); + CHECK(!fallback); + CHECK(atomic_load(&e.conn) == 1); + CHECK(atomic_load(&e.disc) == 1); + CHECK(atomic_load(&t.off) == 1); + CHECK(atomic_load(&t.detach) == 1); + CHECK(atomic_load(&t.disc) == 1); + CHECK(atomic_load(&t.free) == 1); + checkPublished(1, 1, 0); +} + +static void testDead(void) +{ + setRemote(UUID_A); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitReady(fallback)); + CHECK(waitCount(&e.conn, 1)); + + lgTransportFallback_requestVideoActive(fallback, true); + CHECK(waitCount(&t.on, 1)); + atomic_store(&t.alive, false); + lgTransportFallback_requestVideoActive(fallback, true); + CHECK(waitCount(&e.disc, 1)); + + CHECK(!lgTransportFallback_ready(fallback)); + CHECK(!lgTransportFallback_admitted(fallback)); + CHECK(atomic_load(&t.off) == 0); + checkPublished(1, 0, 1); + + lgTransportFallback_stop(&fallback); + CHECK(atomic_load(&e.disc) == 1); + CHECK(atomic_load(&t.detach) == 1); + CHECK(atomic_load(&t.disc) == 1); + CHECK(atomic_load(&t.free) == 1); +} + +static void testMismatch(void) +{ + setRemote(UUID_B); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&t.free, 2)); + + CHECK(!lgTransportFallback_ready(fallback)); + CHECK(!lgTransportFallback_admitted(fallback)); + CHECK(atomic_load(&e.conn) == 0); + CHECK(atomic_load(&e.disc) == 0); + CHECK(atomic_load(&e.mismatch) == 1); + CHECK(memcmp(e.primary, UUID_A, sizeof(UUID_A)) == 0); + CHECK(memcmp(e.fallback, UUID_B, sizeof(UUID_B)) == 0); + checkPublished(0, 0, 0); + + lgTransportFallback_stop(&fallback); + CHECK(atomic_load(&t.conn) == 2); + CHECK(atomic_load(&t.attach) == 2); + CHECK(atomic_load(&t.detach) == 2); + CHECK(atomic_load(&t.disc) == 2); + CHECK(atomic_load(&t.free) == 2); +} + +static void testRevoke(void) +{ + setRemote(UUID_A); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitReady(fallback)); + CHECK(waitCount(&e.conn, 1)); + + lgTransportFallback_setPrimaryUUID(fallback, UUID_B); + lgTransportFallback_setPrimaryUUID(fallback, UUID_B); + CHECK(waitCount(&t.free, 2)); + + CHECK(!lgTransportFallback_ready(fallback)); + CHECK(!lgTransportFallback_admitted(fallback)); + CHECK(atomic_load(&e.conn) == 1); + CHECK(atomic_load(&e.disc) == 1); + CHECK(atomic_load(&e.mismatch) == 1); + CHECK(memcmp(e.primary, UUID_B, sizeof(UUID_B)) == 0); + CHECK(memcmp(e.fallback, UUID_A, sizeof(UUID_A)) == 0); + checkPublished(1, 0, 1); + + lgTransportFallback_stop(&fallback); + CHECK(atomic_load(&t.conn) == 2); + CHECK(atomic_load(&t.detach) == 2); + CHECK(atomic_load(&t.disc) == 2); + CHECK(atomic_load(&t.free) == 2); +} + +static void testRetry(void) +{ + setRemote(UUID_A); + t.failConn = 2; + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitReady(fallback)); + CHECK(waitCount(&e.conn, 1)); + + const uint64_t first = atomic_load(&t.connTime[0]); + const uint64_t second = atomic_load(&t.connTime[1]); + const uint64_t third = atomic_load(&t.connTime[2]); + CHECK(second - first >= UINT64_C(200000000)); + CHECK(third - second >= UINT64_C(400000000)); + CHECK(atomic_load(&t.conn) == 3); + CHECK(atomic_load(&t.free) == 2); + + lgTransportFallback_stop(&fallback); + CHECK(atomic_load(&e.conn) == 1); + CHECK(atomic_load(&e.disc) == 1); + CHECK(atomic_load(&t.attach) == 3); + CHECK(atomic_load(&t.detach) == 3); + CHECK(atomic_load(&t.disc) == 3); + CHECK(atomic_load(&t.free) == 3); + checkPublished(1, 1, 0); +} + +static void testCancel(void) +{ + setRemote(UUID_A); + atomic_store(&t.hold, true); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&t.conn, 1)); + + lgTransportFallback_stop(&fallback); + CHECK(!fallback); + CHECK(atomic_load(&t.cancel)); + CHECK(atomic_load(&e.conn) == 0); + CHECK(atomic_load(&e.disc) == 0); + CHECK(atomic_load(&t.attach) == 1); + CHECK(atomic_load(&t.detach) == 1); + CHECK(atomic_load(&t.disc) == 1); + CHECK(atomic_load(&t.free) == 1); + checkPublished(0, 0, 0); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "graceful", testGraceful }, + { "dead" , testDead }, + { "mismatch", testMismatch }, + { "revoke" , testRevoke }, + { "retry" , testRetry }, + { "cancel" , testCancel }, +}; + +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; +}