From ff47bf6128616d301c18bccf23eaa5f15bb245e3 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 12 Aug 2026 04:03:54 +1000 Subject: [PATCH] [client] spice: test session and surface lifecycle --- client/tests/CMakeLists.txt | 37 ++ client/tests/spice_test.c | 894 ++++++++++++++++++++++++++++++++++++ 2 files changed, 931 insertions(+) create mode 100644 client/tests/spice_test.c diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index a8780c88..565b26ac 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -266,6 +266,43 @@ foreach(name IN LISTS TRANSPORT_FALLBACK_CASES) ) endforeach() +add_executable(spice-tests + spice_test.c + ../transports/SPICE/session.c + ../transports/SPICE/spice.c + ../transports/SPICE/surface.c +) +target_compile_definitions(spice-tests PRIVATE + ENABLE_AUDIO=0 + ENABLE_USB_AUDIO=0 +) +target_include_directories(spice-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../transports/SPICE" + "${PROJECT_TOP}/repos/PureSpice/include" +) +target_link_libraries(spice-tests + ${EXE_FLAGS} + lg_common +) +set(SPICE_CASES + connect-fail + pre-ready-drop + cancel + ready-drop + session-order + surface-active + surface-events + surface-detach +) +foreach(name IN LISTS SPICE_CASES) + add_test(NAME spice-${name} + COMMAND spice-tests ${name} + ) + set_tests_properties(spice-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + add_executable(frame-scheduler-tests frame_scheduler_test.c ../src/frame_scheduler.c diff --git a/client/tests/spice_test.c b/client/tests/spice_test.c new file mode 100644 index 00000000..a6065783 --- /dev/null +++ b/client/tests/spice_test.c @@ -0,0 +1,894 @@ +/** + * 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 "spice.h" +#include "test.h" + +#include "common/debug.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0])) +#define WAIT_MS 2000U + +extern const LG_TransportOps LGT_SPICE; + +struct SpiceInput +{ + bool available; + unsigned int onN; + unsigned int offN; + unsigned int onOrder; + unsigned int offFirst; + unsigned int offOrder; +}; + +struct SpiceClipboard +{ + bool available; + unsigned int onN; + unsigned int offN; + unsigned int offFirst; + unsigned int offOrder; +}; + +struct Pure +{ + PSConfig config; + atomic_bool connectOK; + atomic_bool callReady; + atomic_bool blockConnect; + atomic_bool cancel; + atomic_bool drop; + PSStatus dropStatus; + bool infoOK; + const char * serverName; + uint8_t uuid[16]; + + atomic_uint connectN; + atomic_uint disconnectN; + atomic_uint processN; + atomic_uint mouseModeN; + atomic_uint infoN; + atomic_uint freeInfoN; + atomic_uint order; + unsigned int targetSetOrder; + unsigned int connectOrder; + unsigned int disconnectOrder; + + bool has[PS_CHANNEL_MAX]; + bool connected[PS_CHANNEL_MAX]; + bool connectResult[PS_CHANNEL_MAX]; + bool disconnectResult[PS_CHANNEL_MAX]; + unsigned int connectChannelN[PS_CHANNEL_MAX]; + unsigned int disconnectChannelN[PS_CHANNEL_MAX]; +}; + +struct SurfaceLog +{ + unsigned int configN; + unsigned int destroyN; + unsigned int fillN; + unsigned int bitmapN; + unsigned int pointerN; + unsigned int width; + unsigned int height; + int x; + int y; + int w; + int h; + int stride; + uint32_t color; + bool topDown; + uint8_t bitmap[32]; + size_t bitmapSize; + LG_TransportPointer pointer; + uint8_t shape[32]; + size_t shapeSize; + atomic_bool block; + atomic_bool entered; + atomic_bool release; +}; + +struct Fixture +{ + LG_Transport transport; + SpiceInput input; + SpiceClipboard clipboard; + struct SurfaceLog log; +}; + +static struct Pure ps; +static _Atomic(SpiceClipboard *) cbTarget; + +static unsigned int nextOrder(void) +{ + return atomic_fetch_add(&ps.order, 1) + 1; +} + +void purespice_init(const PSInit * init) +{ + (void)init; +} + +bool purespice_connect(const PSConfig * config) +{ + ps.config = *config; + ps.connectOrder = nextOrder(); + atomic_fetch_add(&ps.connectN, 1); + + while (atomic_load(&ps.blockConnect) && !atomic_load(&ps.cancel)) + usleep(1000); + + if (atomic_load(&ps.cancel) || !atomic_load(&ps.connectOK)) + return false; + if (atomic_load(&ps.callReady) && config->ready) + config->ready(); + return true; +} + +void purespice_disconnect(void) +{ + ps.disconnectOrder = nextOrder(); + atomic_fetch_add(&ps.disconnectN, 1); +} + +PSStatus purespice_process(int timeout) +{ + (void)timeout; + atomic_fetch_add(&ps.processN, 1); + if (atomic_load(&ps.drop)) + return ps.dropStatus; + usleep(1000); + return PS_STATUS_RUN; +} + +bool purespice_getServerInfo(PSServerInfo * info) +{ + atomic_fetch_add(&ps.infoN, 1); + if (!ps.infoOK) + return false; + + memset(info, 0, sizeof(*info)); + if (ps.serverName) + info->name = strdup(ps.serverName); + memcpy(info->uuid, ps.uuid, sizeof(info->uuid)); + return true; +} + +void purespice_freeServerInfo(PSServerInfo * info) +{ + free(info->name); + info->name = NULL; + atomic_fetch_add(&ps.freeInfoN, 1); +} + +bool purespice_hasChannel(PSChannelType channel) +{ + return ps.has[channel]; +} + +bool purespice_channelConnected(PSChannelType channel) +{ + return ps.connected[channel]; +} + +bool purespice_connectChannel(PSChannelType channel) +{ + ++ps.connectChannelN[channel]; + if (!ps.connectResult[channel]) + return false; + ps.connected[channel] = true; + return true; +} + +bool purespice_disconnectChannel(PSChannelType channel) +{ + ++ps.disconnectChannelN[channel]; + if (!ps.disconnectResult[channel]) + return false; + ps.connected[channel] = false; + return true; +} + +bool purespice_mouseMode(bool server) +{ + (void)server; + atomic_fetch_add(&ps.mouseModeN, 1); + return true; +} + +bool spiceInput_init(SpiceInput ** input) +{ + *input = calloc(1, sizeof(**input)); + return *input != NULL; +} + +void spiceInput_free(SpiceInput ** input) +{ + if (!input) + return; + free(*input); + *input = NULL; +} + +const LG_InputOps * spiceInput_getOps(void) +{ + static const LG_InputOps ops = { .name = "test" }; + return &ops; +} + +void spiceInput_setAvailable(SpiceInput * input, bool available) +{ + input->available = available; + if (available) + { + ++input->onN; + input->onOrder = nextOrder(); + } + else + { + ++input->offN; + input->offOrder = nextOrder(); + if (input->offN == 1) + input->offFirst = input->offOrder; + } +} + +bool spiceClipboard_init(SpiceClipboard ** clipboard) +{ + *clipboard = calloc(1, sizeof(**clipboard)); + return *clipboard != NULL; +} + +void spiceClipboard_free(SpiceClipboard ** clipboard) +{ + if (!clipboard) + return; + free(*clipboard); + *clipboard = NULL; +} + +const LG_ClipboardOps * spiceClipboard_getOps(void) +{ + static const LG_ClipboardOps ops = { .name = "test" }; + return &ops; +} + +void spiceClipboard_setAvailable(SpiceClipboard * clipboard, bool available) +{ + clipboard->available = available; + if (available) + ++clipboard->onN; + else + { + ++clipboard->offN; + clipboard->offOrder = nextOrder(); + if (clipboard->offN == 1) + clipboard->offFirst = clipboard->offOrder; + } +} + +void spiceClipboard_setCallbackTarget(SpiceClipboard * clipboard) +{ + atomic_store(&cbTarget, clipboard); + if (clipboard) + ps.targetSetOrder = nextOrder(); +} + +void spiceClipboard_notice(PSDataType type) +{ + (void)type; +} + +void spiceClipboard_data(PSDataType type, uint8_t * buffer, uint32_t size) +{ + (void)type; + (void)buffer; + (void)size; +} + +void spiceClipboard_release(void) +{ +} + +void spiceClipboard_request(PSDataType type) +{ + (void)type; +} + +void spiceClipboard_status(bool available) +{ + SpiceClipboard * clipboard = atomic_load(&cbTarget); + if (clipboard) + spiceClipboard_setAvailable(clipboard, available); +} + +static void resetPure(void) +{ + memset(&ps, 0, sizeof(ps)); + atomic_init(&ps.connectOK, true); + atomic_init(&ps.callReady, true); + atomic_init(&ps.blockConnect, false); + atomic_init(&ps.cancel, false); + atomic_init(&ps.drop, false); + atomic_init(&ps.connectN, 0); + atomic_init(&ps.disconnectN, 0); + atomic_init(&ps.processN, 0); + atomic_init(&ps.mouseModeN, 0); + atomic_init(&ps.infoN, 0); + atomic_init(&ps.freeInfoN, 0); + atomic_init(&ps.order, 0); + atomic_store(&cbTarget, NULL); + + ps.dropStatus = PS_STATUS_ERR_READ; + ps.infoOK = true; + ps.serverName = "test-vm"; + ps.uuid[0] = 0x42; + for (unsigned int i = 0; i < PS_CHANNEL_MAX; ++i) + { + ps.has[i] = true; + ps.connectResult[i] = true; + ps.disconnectResult[i] = true; + } +} + +static void configure(void * opaque, unsigned int width, + unsigned int height) +{ + struct SurfaceLog * log = opaque; + ++log->configN; + log->width = width; + log->height = height; + if (atomic_load(&log->block)) + { + atomic_store(&log->entered, true); + while (!atomic_load(&log->release)) + usleep(1000); + } +} + +static void destroy(void * opaque) +{ + struct SurfaceLog * log = opaque; + ++log->destroyN; +} + +static void drawFill(void * opaque, int x, int y, + int width, int height, uint32_t color) +{ + struct SurfaceLog * log = opaque; + ++log->fillN; + log->x = x; + log->y = y; + log->w = width; + log->h = height; + log->color = color; +} + +static void drawBitmap(void * opaque, bool topDown, + int x, int y, int width, int height, int stride, const void * data) +{ + struct SurfaceLog * log = opaque; + ++log->bitmapN; + log->topDown = topDown; + log->x = x; + log->y = y; + log->w = width; + log->h = height; + log->stride = stride; + log->bitmapSize = (size_t)height * (size_t)stride; + CHECK(log->bitmapSize <= sizeof(log->bitmap)); + memcpy(log->bitmap, data, log->bitmapSize); +} + +static void pointer(void * opaque, const LG_TransportPointer * pointer) +{ + struct SurfaceLog * log = opaque; + ++log->pointerN; + log->pointer = *pointer; + if (pointer->shape) + { + log->shapeSize = (size_t)pointer->height * pointer->pitch; + CHECK(log->shapeSize <= sizeof(log->shape)); + memcpy(log->shape, pointer->shape, log->shapeSize); + log->pointer.shape = log->shape; + } +} + +static const LG_SwSurfaceEventOps surfaceEvents = +{ + .configure = configure, + .destroy = destroy, + .drawFill = drawFill, + .drawBitmap = drawBitmap, + .pointer = pointer, +}; + +static void initFixture(struct Fixture * f) +{ + memset(f, 0, sizeof(*f)); + f->transport.host = "test"; + f->transport.port = 5900; + f->transport.inputEnabled = true; + f->transport.clipboardEnabled = true; + f->transport.input = &f->input; + f->transport.clipboard = &f->clipboard; + CHECK(spiceSurface_init(&f->transport.surface)); + f->transport.connectEvent = lgCreateEvent(false, 0); + CHECK(f->transport.connectEvent); + atomic_init(&f->transport.stop, false); + atomic_init(&f->transport.sessionValid, false); + atomic_init(&f->log.block, false); + atomic_init(&f->log.entered, false); + atomic_init(&f->log.release, false); +} + +static void freeFixture(struct Fixture * f) +{ + LGT_SPICE.disconnect(&f->transport); + lgFreeEvent(f->transport.connectEvent); + f->transport.connectEvent = NULL; + spiceSurface_free(&f->transport.surface); +} + +static void waitInvalid(LG_Transport * transport) +{ + for (unsigned int i = 0; i < WAIT_MS; ++i) + { + if (!LGT_SPICE.sessionValid(transport)) + return; + usleep(1000); + } + + CHECK(!LGT_SPICE.sessionValid(transport)); +} + +static void waitBool(atomic_bool * value) +{ + for (unsigned int i = 0; i < WAIT_MS; ++i) + { + if (atomic_load_explicit(value, memory_order_acquire)) + return; + usleep(1000); + } + + CHECK(atomic_load(value)); +} + +static bool cancel(void * opaque) +{ + unsigned int * calls = opaque; + ++*calls; + atomic_store(&ps.cancel, true); + return true; +} + +static void testConnectFail(void) +{ + resetPure(); + atomic_store(&ps.connectOK, false); + struct Fixture f; + initFixture(&f); + + LG_TransportSession session; + CHECK(LGT_SPICE.connect(&f.transport, &session) == LG_TRANSPORT_ERROR); + CHECK(!f.transport.thread); + CHECK(!LGT_SPICE.sessionValid(&f.transport)); + CHECK(atomic_load(&ps.connectN) == 1); + CHECK(atomic_load(&ps.disconnectN) == 0); + CHECK(f.input.onN == 0); + CHECK(f.input.offN == 1); + CHECK(f.clipboard.offN == 1); + CHECK(!atomic_load(&cbTarget)); + CHECK(ps.targetSetOrder < ps.connectOrder); + + ps.config.ready(); + CHECK(f.input.onN == 0); + CHECK(!LGT_SPICE.sessionValid(&f.transport)); + freeFixture(&f); +} + +static void testPreReadyDrop(void) +{ + resetPure(); + atomic_store(&ps.callReady, false); + atomic_store(&ps.drop, true); + ps.dropStatus = PS_STATUS_SHUTDOWN; + struct Fixture f; + initFixture(&f); + + LG_TransportSession session; + CHECK(LGT_SPICE.connect(&f.transport, &session) == LG_TRANSPORT_ERROR); + CHECK(!f.transport.thread); + CHECK(!LGT_SPICE.sessionValid(&f.transport)); + CHECK(atomic_load(&ps.connectN) == 1); + CHECK(atomic_load(&ps.processN) == 1); + CHECK(atomic_load(&ps.disconnectN) == 1); + CHECK(f.input.onN == 0); + CHECK(f.input.offN == 2); + CHECK(f.clipboard.offN == 2); + CHECK(f.input.offFirst < ps.disconnectOrder); + CHECK(f.clipboard.offFirst < ps.disconnectOrder); + CHECK(!atomic_load(&cbTarget)); + freeFixture(&f); +} + +static void testCancel(void) +{ + resetPure(); + atomic_store(&ps.blockConnect, true); + struct Fixture f; + initFixture(&f); + unsigned int cancelN = 0; + LG_TransportSession session; + + CHECK(LGT_SPICE.connectCancellable(&f.transport, &session, + cancel, &cancelN) == LG_TRANSPORT_DISCONNECTED); + CHECK(cancelN == 1); + CHECK(!f.transport.thread); + CHECK(!LGT_SPICE.sessionValid(&f.transport)); + CHECK(atomic_load(&ps.connectN) == 1); + CHECK(atomic_load(&ps.disconnectN) == 0); + CHECK(f.input.onN == 0); + CHECK(f.input.offN == 1); + CHECK(f.clipboard.offN == 1); + CHECK(!atomic_load(&cbTarget)); + freeFixture(&f); +} + +static void testReadyDrop(void) +{ + resetPure(); + struct Fixture f; + initFixture(&f); + const LG_VideoOps * video = LGT_SPICE.getVideoOps(&f.transport); + CHECK(video && video->type == LG_VIDEO_TYPE_SW_SURFACE); + CHECK(video->swSurface->attach( + &f.transport, &surfaceEvents, &f.log)); + + LG_TransportSession session; + CHECK(LGT_SPICE.connect(&f.transport, &session) == LG_TRANSPORT_OK); + CHECK(LGT_SPICE.sessionValid(&f.transport)); + CHECK(strcmp(session.version, "SPICE") == 0); + CHECK(strcmp(session.name, "test-vm") == 0); + CHECK(session.uuidValid); + CHECK(session.uuid[0] == 0x42); + CHECK(session.os == LG_TRANSPORT_OS_OTHER); + CHECK(f.input.available); + CHECK(f.input.onN == 1); + CHECK(atomic_load(&ps.mouseModeN) == 1); + CHECK(atomic_load(&ps.infoN) == 1); + CHECK(atomic_load(&ps.freeInfoN) == 1); + CHECK(ps.targetSetOrder < ps.connectOrder); + CHECK(ps.connectOrder < f.input.onOrder); + + ps.config.clipboard.status(true); + CHECK(f.clipboard.available); + ps.config.display.surfaceCreate( + 0, PS_SURFACE_FMT_32_xRGB, 640, 480); + CHECK(f.log.configN == 1); + CHECK(video->swSurface->setActive(&f.transport, true)); + + atomic_store(&ps.drop, true); + waitInvalid(&f.transport); + LGT_SPICE.disconnect(&f.transport); + CHECK(!f.transport.thread); + CHECK(!f.input.available); + CHECK(!f.clipboard.available); + CHECK(f.input.offN >= 1); + CHECK(f.clipboard.offN >= 1); + CHECK(atomic_load(&ps.disconnectN) == 1); + CHECK(f.input.offFirst < ps.disconnectOrder); + CHECK(f.clipboard.offFirst < ps.disconnectOrder); + CHECK(!atomic_load(&cbTarget)); + CHECK(LGT_SPICE.sendControl(&f.transport, NULL, NULL) == + LG_TRANSPORT_DISCONNECTED); + + const unsigned int inputOn = f.input.onN; + const unsigned int configN = f.log.configN; + ps.config.ready(); + ps.config.display.surfaceCreate( + 0, PS_SURFACE_FMT_32_xRGB, 800, 600); + CHECK(f.input.onN == inputOn); + CHECK(f.log.configN == configN); + video->swSurface->detach(&f.transport); + freeFixture(&f); +} + +static void testSessionOrder(void) +{ + resetPure(); + struct Fixture first; + struct Fixture second; + initFixture(&first); + initFixture(&second); + + LG_TransportSession session; + CHECK(LGT_SPICE.connect(&first.transport, &session) == LG_TRANSPORT_OK); + CHECK(LGT_SPICE.connect(&second.transport, &session) == + LG_TRANSPORT_ERROR); + CHECK(!second.transport.thread); + CHECK(atomic_load(&ps.connectN) == 1); + CHECK(LGT_SPICE.sessionValid(&first.transport)); + CHECK(!LGT_SPICE.sessionValid(&second.transport)); + + LGT_SPICE.disconnect(&first.transport); + CHECK(!LGT_SPICE.sessionValid(&first.transport)); + CHECK(LGT_SPICE.connect(&second.transport, &session) == LG_TRANSPORT_OK); + CHECK(atomic_load(&ps.connectN) == 2); + CHECK(LGT_SPICE.sessionValid(&second.transport)); + LGT_SPICE.disconnect(&second.transport); + freeFixture(&second); + freeFixture(&first); +} + +static void testSurfaceActive(void) +{ + resetPure(); + struct Fixture f; + initFixture(&f); + const LG_SwSurfaceOps * sw = + spiceSurface_getVideoOps()->swSurface; + + LG_SwSurfaceEventOps bad = surfaceEvents; + bad.pointer = NULL; + CHECK(!sw->attach(&f.transport, &bad, &f.log)); + CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log)); + CHECK(!sw->setActive(&f.transport, true)); + atomic_store(&f.transport.sessionValid, true); + + ps.has[PS_CHANNEL_DISPLAY] = false; + CHECK(!sw->setActive(&f.transport, true)); + ps.has[PS_CHANNEL_DISPLAY] = true; + ps.connectResult[PS_CHANNEL_DISPLAY] = false; + CHECK(!sw->setActive(&f.transport, true)); + CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == 1); + CHECK(ps.connectChannelN[PS_CHANNEL_CURSOR] == 0); + + ps.connectResult[PS_CHANNEL_DISPLAY] = true; + ps.connectResult[PS_CHANNEL_CURSOR] = false; + CHECK(!sw->setActive(&f.transport, true)); + CHECK(ps.disconnectChannelN[PS_CHANNEL_DISPLAY] == 1); + CHECK(!ps.connected[PS_CHANNEL_DISPLAY]); + + ps.connectResult[PS_CHANNEL_CURSOR] = true; + CHECK(sw->setActive(&f.transport, true)); + const unsigned int displayConnect = + ps.connectChannelN[PS_CHANNEL_DISPLAY]; + const unsigned int cursorConnect = + ps.connectChannelN[PS_CHANNEL_CURSOR]; + CHECK(sw->setActive(&f.transport, true)); + CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == displayConnect); + CHECK(ps.connectChannelN[PS_CHANNEL_CURSOR] == cursorConnect); + + ps.disconnectResult[PS_CHANNEL_DISPLAY] = false; + CHECK(!sw->setActive(&f.transport, false)); + CHECK(ps.disconnectChannelN[PS_CHANNEL_CURSOR] == 0); + ps.disconnectResult[PS_CHANNEL_DISPLAY] = true; + ps.disconnectResult[PS_CHANNEL_CURSOR] = false; + CHECK(!sw->setActive(&f.transport, false)); + CHECK(ps.connected[PS_CHANNEL_DISPLAY]); + CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == displayConnect + 1); + + ps.disconnectResult[PS_CHANNEL_CURSOR] = true; + CHECK(sw->setActive(&f.transport, false)); + CHECK(!ps.connected[PS_CHANNEL_DISPLAY]); + CHECK(!ps.connected[PS_CHANNEL_CURSOR]); + sw->detach(&f.transport); + freeFixture(&f); +} + +static void testSurfaceEvents(void) +{ + resetPure(); + struct Fixture f; + initFixture(&f); + const LG_SwSurfaceOps * sw = + spiceSurface_getVideoOps()->swSurface; + CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log)); + + spiceSurface_create(f.transport.surface, + 1, PS_SURFACE_FMT_32_xRGB, 20, 10); + CHECK(f.log.configN == 0); + spiceSurface_create(f.transport.surface, + 0, PS_SURFACE_FMT_32_xRGB, 2, 1); + CHECK(f.log.configN == 1); + CHECK(f.log.width == 2); + CHECK(f.log.height == 1); + CHECK(f.log.fillN == 1); + + spiceSurface_drawFill( + f.transport.surface, 0, 1, 2, 3, 4, 0xaabbccdd); + CHECK(f.log.fillN == 2); + CHECK(f.log.x == 1); + CHECK(f.log.y == 2); + CHECK(f.log.w == 3); + CHECK(f.log.h == 4); + CHECK(f.log.color == 0xaabbccdd); + + uint8_t bitmap[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + spiceSurface_drawBitmap(f.transport.surface, 0, + PS_BITMAP_FMT_24BIT, true, 0, 0, 2, 1, 8, bitmap); + CHECK(f.log.bitmapN == 0); + spiceSurface_drawBitmap(f.transport.surface, 0, + PS_BITMAP_FMT_RGBA, true, 4, 5, 2, 1, 8, bitmap); + CHECK(f.log.bitmapN == 1); + CHECK(f.log.topDown); + CHECK(f.log.x == 4); + CHECK(f.log.y == 5); + CHECK(f.log.bitmapSize == sizeof(bitmap)); + CHECK(memcmp(f.log.bitmap, bitmap, sizeof(bitmap)) == 0); + + const uint8_t rgba[] = + { + 1, 2, 3, 4, + 5, 6, 7, 8, + }; + const uint8_t bgra[] = + { + 3, 2, 1, 4, + 7, 6, 5, 8, + }; + spiceSurface_setRGBAImage( + f.transport.surface, 2, 1, 6, 7, rgba); + CHECK(f.log.pointerN == 1); + CHECK(f.log.pointer.flags == LG_TRANSPORT_POINTER_SHAPE); + CHECK(f.log.pointer.type == CURSOR_TYPE_COLOR); + CHECK(f.log.pointer.hx == 6); + CHECK(f.log.pointer.hy == 7); + CHECK(f.log.shapeSize == sizeof(bgra)); + CHECK(memcmp(f.log.shape, bgra, sizeof(bgra)) == 0); + + spiceSurface_setPointerState(f.transport.surface, true, 8, 9); + CHECK(f.log.pointerN == 2); + CHECK(f.log.pointer.flags & LG_TRANSPORT_POINTER_VISIBLE); + CHECK(f.log.pointer.x == 8); + CHECK(f.log.pointer.y == 9); + CHECK(f.log.pointer.hx == 6); + CHECK(f.log.pointer.hy == 7); + + spiceSurface_destroy(f.transport.surface, 0); + CHECK(f.log.destroyN == 1); + spiceSurface_drawFill(f.transport.surface, 0, 0, 0, 1, 1, 0); + CHECK(f.log.fillN == 2); + sw->detach(&f.transport); + freeFixture(&f); +} + +struct CreateCall +{ + SpiceSurface * surface; +}; + +static void * createSurface(void * opaque) +{ + struct CreateCall * call = opaque; + spiceSurface_create( + call->surface, 0, PS_SURFACE_FMT_32_xRGB, 640, 480); + return NULL; +} + +struct DetachCall +{ + LG_Transport * transport; + const LG_SwSurfaceOps * ops; + atomic_bool started; + atomic_bool done; +}; + +static void * detachSurface(void * opaque) +{ + struct DetachCall * call = opaque; + atomic_store(&call->started, true); + call->ops->detach(call->transport); + atomic_store(&call->done, true); + return NULL; +} + +static void testSurfaceDetach(void) +{ + resetPure(); + struct Fixture f; + initFixture(&f); + const LG_SwSurfaceOps * sw = + spiceSurface_getVideoOps()->swSurface; + CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log)); + atomic_store(&f.log.block, true); + + struct CreateCall create = { .surface = f.transport.surface }; + pthread_t createThread; + CHECK(pthread_create(&createThread, NULL, createSurface, &create) == 0); + waitBool(&f.log.entered); + + struct DetachCall detach = + { + .transport = &f.transport, + .ops = sw, + }; + atomic_init(&detach.started, false); + atomic_init(&detach.done, false); + pthread_t detachThread; + CHECK(pthread_create(&detachThread, NULL, detachSurface, &detach) == 0); + waitBool(&detach.started); + usleep(10000); + CHECK(!atomic_load(&detach.done)); + + atomic_store(&f.log.release, true); + CHECK(pthread_join(createThread, NULL) == 0); + CHECK(pthread_join(detachThread, NULL) == 0); + CHECK(atomic_load(&detach.done)); + CHECK(f.log.configN == 1); + CHECK(f.log.fillN == 1); + spiceSurface_drawFill(f.transport.surface, 0, 0, 0, 1, 1, 0); + CHECK(f.log.fillN == 1); + freeFixture(&f); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "connect-fail" , testConnectFail }, + { "pre-ready-drop", testPreReadyDrop }, + { "cancel" , testCancel }, + { "ready-drop" , testReadyDrop }, + { "session-order" , testSessionOrder }, + { "surface-active", testSurfaceActive }, + { "surface-events", testSurfaceEvents }, + { "surface-detach", testSurfaceDetach }, +}; + +int main(int argc, char ** argv) +{ + debug_init(); + + if (argc == 2) + { + for (size_t 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; + } + + if (argc != 1) + return EXIT_FAILURE; + + for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i) + tests[i].run(); + return 0; +}