Files
LookingGlass/client/tests/transport_fallback_test.c
2026-08-12 05:05:26 +10:00

614 lines
15 KiB
C

/**
* 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 <stdatomic.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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 <case>\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;
}