/** * 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 == 0); spiceSurface_drawFill( f.transport.surface, 0, 1, 2, 3, 4, 0xaabbccdd); CHECK(f.log.fillN == 1); 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 == 1); 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 == 0); spiceSurface_drawFill(f.transport.surface, 0, 0, 0, 1, 1, 0); CHECK(f.log.fillN == 0); 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; }