diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index c0c2ec79..a42c19bc 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -167,6 +167,37 @@ if(ENABLE_WAYLAND) endforeach() endif() +if(ENABLE_X11) + add_executable(x11-clipboard-tests + x11_clipboard_test.c + ../displayservers/X11/clipboard.c + ) + target_include_directories(x11-clipboard-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../displayservers/X11" + ) + target_link_libraries(x11-clipboard-tests + ${EXE_FLAGS} + lg_common + ) + set(X11_CLIPBOARD_CASES + targets + normal + incr + malformed + replace + source + teardown + ) + foreach(name IN LISTS X11_CLIPBOARD_CASES) + add_test(NAME x11-clipboard-${name} + COMMAND x11-clipboard-tests ${name} + ) + set_tests_properties(x11-clipboard-${name} PROPERTIES + TIMEOUT 10 + ) + endforeach() +endif() + add_executable(displayserver-input-tests displayserver_input_test.c ../displayservers/Wayland/input_event.c diff --git a/client/tests/x11_clipboard_test.c b/client/tests/x11_clipboard_test.c new file mode 100644 index 00000000..78e41d05 --- /dev/null +++ b/client/tests/x11_clipboard_test.c @@ -0,0 +1,821 @@ +/** + * 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 "atoms.h" +#include "clipboard.h" +#include "test.h" +#include "x11.h" + +#include "common/debug.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0])) +#define MAX_ABORT 12U +#define MAX_CHANGE 8U +#define MAX_CONVERT 16U +#define MAX_DATA 4U +#define MAX_NOTICE 8U +#define MAX_PROP 16U +#define MAX_SEND 8U +#define MAX_TRANSFER 8192U +#define MAX_WINDOW 16U + +#define A_CLIPBOARD 10UL +#define A_TARGETS 11UL +#define A_SEL_DATA 12UL +#define A_INCR 13UL +#define A_TEXT 100UL +#define A_PNG 101UL +#define A_BMP 102UL +#define A_TIFF 103UL +#define A_JPEG 104UL + +struct WindowLog +{ + Window window; + bool dead; +}; + +struct ConvertLog +{ + Atom selection; + Atom target; + Atom property; + Window requestor; +}; + +struct ChangeLog +{ + Window window; + Atom property; + Atom type; + int format; + int count; + unsigned char data[MAX_TRANSFER]; + size_t size; +}; + +struct Property +{ + int status; + Atom type; + int format; + unsigned long count; + unsigned long after; + unsigned char data[MAX_TRANSFER]; + size_t size; + bool null; +}; + +struct TypeNotice +{ + LG_ClipboardData type[LG_CLIPBOARD_DATA_NONE]; + int count; +}; + +struct DataNotice +{ + LG_ClipboardRequest request; + LG_ClipboardData type; + uint8_t data[MAX_TRANSFER]; + size_t size; +}; + +struct Log +{ + struct WindowLog window[MAX_WINDOW]; + struct ConvertLog convert[MAX_CONVERT]; + struct ChangeLog change[MAX_CHANGE]; + struct Property prop[MAX_PROP]; + struct TypeNotice notice[MAX_NOTICE]; + struct DataNotice data[MAX_DATA]; + XEvent send[MAX_SEND]; + LG_ClipboardRequest abort[MAX_ABORT]; + unsigned int windowN; + unsigned int convertN; + unsigned int changeN; + unsigned int propN; + unsigned int propPos; + unsigned int noticeN; + unsigned int dataN; + unsigned int sendN; + unsigned int abortN; + unsigned int releaseN; + unsigned int requestN; + unsigned int destroyN; + unsigned int freeN; + unsigned int flushN; + unsigned int grabN; + unsigned int ungrabN; + unsigned int selectN; + LG_ClipboardData requestType; + LG_ClipboardReplyFn reply; + void * replyOpaque; + Window owner; + Window nextWindow; + bool requestOK; + bool fixesOK; +}; + +struct X11DSState x11; +struct X11DSAtoms x11atoms; + +static struct Log rec; + +static size_t itemSize(int format, unsigned long count) +{ + switch (format) + { + case 8: + return count; + case 16: + return count * sizeof(short); + case 32: + return count * sizeof(long); + default: + return 0; + } +} + +Atom XInternAtom(Display * display, const char * name, Bool onlyIfExists) +{ + CHECK(display == x11.display); + CHECK(!onlyIfExists); + if (!strcmp(name, "UTF8_STRING")) + return A_TEXT; + if (!strcmp(name, "image/png")) + return A_PNG; + if (!strcmp(name, "image/bmp")) + return A_BMP; + if (!strcmp(name, "image/tiff")) + return A_TIFF; + if (!strcmp(name, "image/jpeg")) + return A_JPEG; + CHECK(false); + return None; +} + +Bool XFixesQueryExtension(Display * display, int * eventBase, int * errorBase) +{ + CHECK(display == x11.display); + if (!rec.fixesOK) + return False; + *eventBase = 80; + *errorBase = 81; + return True; +} + +void XFixesSelectSelectionInput(Display * display, Window window, + Atom selection, unsigned long mask) +{ + CHECK(display == x11.display); + CHECK(window == x11.window); + CHECK(selection == x11atoms.CLIPBOARD); + CHECK(mask == XFixesSetSelectionOwnerNotifyMask); + ++rec.selectN; +} + +Window XCreateSimpleWindow(Display * display, Window parent, + int x, int y, unsigned int width, unsigned int height, + unsigned int borderWidth, unsigned long border, + unsigned long background) +{ + CHECK(display == x11.display); + CHECK(parent == x11.window); + CHECK(x == 0); + CHECK(y == 0); + CHECK(width == 1); + CHECK(height == 1); + CHECK(borderWidth == 0); + CHECK(border == 0); + CHECK(background == 0); + CHECK(rec.windowN < ARRAY_LENGTH(rec.window)); + struct WindowLog * window = &rec.window[rec.windowN++]; + window->window = ++rec.nextWindow; + return window->window; +} + +int XDestroyWindow(Display * display, Window window) +{ + CHECK(display == x11.display); + for (unsigned int i = 0; i < rec.windowN; ++i) + if (rec.window[i].window == window) + { + CHECK(!rec.window[i].dead); + rec.window[i].dead = true; + ++rec.destroyN; + return Success; + } + CHECK(false); + return BadWindow; +} + +int XConvertSelection(Display * display, Atom selection, Atom target, + Atom property, Window requestor, Time time) +{ + CHECK(display == x11.display); + CHECK(time == CurrentTime); + CHECK(rec.convertN < ARRAY_LENGTH(rec.convert)); + struct ConvertLog * convert = &rec.convert[rec.convertN++]; + convert->selection = selection; + convert->target = target; + convert->property = property; + convert->requestor = requestor; + return Success; +} + +int XSelectInput(Display * display, Window window, long mask) +{ + CHECK(display == x11.display); + CHECK(mask == PropertyChangeMask); + bool found = false; + for (unsigned int i = 0; i < rec.windowN; ++i) + found |= rec.window[i].window == window && !rec.window[i].dead; + CHECK(found); + return Success; +} + +int XSetSelectionOwner(Display * display, Atom selection, + Window owner, Time time) +{ + CHECK(display == x11.display); + CHECK(selection == x11atoms.CLIPBOARD); + CHECK(time == CurrentTime); + rec.owner = owner; + return Success; +} + +Window XGetSelectionOwner(Display * display, Atom selection) +{ + CHECK(display == x11.display); + CHECK(selection == x11atoms.CLIPBOARD); + return rec.owner; +} + +int XGrabServer(Display * display) +{ + CHECK(display == x11.display); + ++rec.grabN; + return Success; +} + +int XUngrabServer(Display * display) +{ + CHECK(display == x11.display); + ++rec.ungrabN; + return Success; +} + +int XFlush(Display * display) +{ + CHECK(display == x11.display); + ++rec.flushN; + return Success; +} + +int XChangeProperty(Display * display, Window window, Atom property, + Atom type, int format, int mode, const unsigned char * data, int count) +{ + CHECK(display == x11.display); + CHECK(mode == PropModeReplace); + CHECK(count >= 0); + CHECK(rec.changeN < ARRAY_LENGTH(rec.change)); + struct ChangeLog * change = &rec.change[rec.changeN++]; + change->window = window; + change->property = property; + change->type = type; + change->format = format; + change->count = count; + change->size = itemSize(format, count); + CHECK(change->size <= sizeof(change->data)); + CHECK(!change->size || data); + if (change->size) + memcpy(change->data, data, change->size); + return Success; +} + +Status XSendEvent(Display * display, Window window, Bool propagate, + long mask, XEvent * event) +{ + CHECK(display == x11.display); + CHECK(!propagate); + CHECK(mask == 0); + CHECK(rec.sendN < ARRAY_LENGTH(rec.send)); + CHECK(event->xselection.requestor == window); + rec.send[rec.sendN++] = *event; + return True; +} + +int XGetWindowProperty(Display * display, Window window, Atom property, + long offset, long length, Bool delete, Atom requestedType, + Atom * actualType, int * actualFormat, unsigned long * count, + unsigned long * after, unsigned char ** data) +{ + CHECK(display == x11.display); + CHECK(window != None); + CHECK(property != None); + CHECK(offset == 0); + CHECK(length == ~0L); + CHECK(delete); + CHECK(requestedType == AnyPropertyType); + CHECK(rec.propPos < rec.propN); + const struct Property * prop = &rec.prop[rec.propPos++]; + *actualType = prop->type; + *actualFormat = prop->format; + *count = prop->count; + *after = prop->after; + if (prop->null) + *data = NULL; + else + { + CHECK(prop->size <= sizeof(prop->data)); + *data = malloc(prop->size ? prop->size : 1); + CHECK(*data); + if (prop->size) + memcpy(*data, prop->data, prop->size); + } + return prop->status; +} + +int XFree(void * data) +{ + CHECK(data); + ++rec.freeN; + free(data); + return Success; +} + +void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count) +{ + CHECK(rec.noticeN < ARRAY_LENGTH(rec.notice)); + CHECK(count >= 0); + CHECK(count <= LG_CLIPBOARD_DATA_NONE); + struct TypeNotice * notice = &rec.notice[rec.noticeN++]; + notice->count = count; + memcpy(notice->type, types, (size_t)count * sizeof(*types)); +} + +void app_clipboardData(LG_ClipboardRequest request, + LG_ClipboardData type, const void * data, size_t size) +{ + CHECK(rec.dataN < ARRAY_LENGTH(rec.data)); + CHECK(size <= MAX_TRANSFER); + CHECK(!size || data); + struct DataNotice * notice = &rec.data[rec.dataN++]; + notice->request = request; + notice->type = type; + notice->size = size; + if (size) + memcpy(notice->data, data, size); +} + +void app_clipboardAbort(LG_ClipboardRequest request) +{ + CHECK(rec.abortN < ARRAY_LENGTH(rec.abort)); + rec.abort[rec.abortN++] = request; +} + +void app_clipboardRelease(void) +{ + ++rec.releaseN; +} + +bool app_clipboardRequest(LG_ClipboardData type, + LG_ClipboardReplyFn replyFn, void * opaque) +{ + ++rec.requestN; + rec.requestType = type; + rec.reply = replyFn; + rec.replyOpaque = opaque; + return rec.requestOK; +} + +static struct Property * prop(void) +{ + CHECK(rec.propN < ARRAY_LENGTH(rec.prop)); + struct Property * prop = &rec.prop[rec.propN++]; + prop->status = Success; + return prop; +} + +static void prop8(Atom type, const void * data, size_t size) +{ + struct Property * value = prop(); + CHECK(size <= sizeof(value->data)); + value->type = type; + value->format = 8; + value->count = size; + value->size = size; + if (size) + memcpy(value->data, data, size); +} + +static void prop32(Atom type, const unsigned long * data, size_t count) +{ + struct Property * value = prop(); + const size_t size = count * sizeof(*data); + CHECK(size <= sizeof(value->data)); + value->type = type; + value->format = 32; + value->count = count; + value->size = size; + if (size) + memcpy(value->data, data, size); +} + +static void propNull(Atom type, int format, unsigned long count) +{ + struct Property * value = prop(); + value->type = type; + value->format = format; + value->count = count; + value->null = true; +} + +static void selection(Window requestor, Atom target, Atom property) +{ + XEvent event = {}; + event.xselection.type = SelectionNotify; + event.xselection.display = x11.display; + event.xselection.requestor = requestor; + event.xselection.selection = x11atoms.CLIPBOARD; + event.xselection.target = target; + event.xselection.property = property; + CHECK(x11CBEventThread(&event)); +} + +static void property(Window window) +{ + XEvent event = {}; + event.xproperty.type = PropertyNotify; + event.xproperty.display = x11.display; + event.xproperty.window = window; + event.xproperty.atom = x11atoms.SEL_DATA; + event.xproperty.state = PropertyNewValue; + CHECK(x11CBEventThread(&event)); +} + +static void owner(Window owner) +{ + rec.owner = owner; + XFixesSelectionNotifyEvent event = + { + .type = x11.eventBase + XFixesSelectionNotify, + .display = x11.display, + .window = x11.window, + .owner = owner, + .selection = x11atoms.CLIPBOARD, + }; + CHECK(x11CBEventThread((const XEvent *)&event)); +} + +static void discover(Window ownerWindow, + const unsigned long * targets, size_t count) +{ + const unsigned int no = rec.convertN; + owner(ownerWindow); + CHECK(rec.convertN == no + 1); + const struct ConvertLog * convert = &rec.convert[no]; + CHECK(convert->selection == x11atoms.CLIPBOARD); + CHECK(convert->target == x11atoms.TARGETS); + CHECK(convert->property == x11atoms.TARGETS); + prop32(XA_ATOM, targets, count); + selection(convert->requestor, x11atoms.TARGETS, x11atoms.TARGETS); +} + +static Window request(LG_ClipboardRequest id, LG_ClipboardData type) +{ + const unsigned int no = rec.convertN; + x11CBRequest(id, type); + CHECK(rec.convertN == no + 1); + const struct ConvertLog * convert = &rec.convert[no]; + CHECK(convert->selection == x11atoms.CLIPBOARD); + CHECK(convert->property == x11atoms.SEL_DATA); + return convert->requestor; +} + +static void selectionRequest(Atom target, Atom property) +{ + XEvent event = {}; + event.xselectionrequest.type = SelectionRequest; + event.xselectionrequest.display = x11.display; + event.xselectionrequest.owner = x11.window; + event.xselectionrequest.requestor = 900; + event.xselectionrequest.selection = x11atoms.CLIPBOARD; + event.xselectionrequest.target = target; + event.xselectionrequest.property = property; + CHECK(x11CBEventThread(&event)); +} + +static void start(void) +{ + memset(&x11, 0, sizeof(x11)); + memset(&x11atoms, 0, sizeof(x11atoms)); + memset(&rec, 0, sizeof(rec)); + rec.nextWindow = 1000; + rec.requestOK = true; + rec.fixesOK = true; + + x11.display = (Display *)(uintptr_t)1; + x11.window = 50; + + x11atoms.CLIPBOARD = A_CLIPBOARD; + x11atoms.TARGETS = A_TARGETS; + x11atoms.SEL_DATA = A_SEL_DATA; + x11atoms.INCR = A_INCR; + + CHECK(x11CBInit()); + CHECK(x11.eventBase == 80); + CHECK(x11.errorBase == 81); + CHECK(rec.selectN == 1); +} + +static void finish(void) +{ + x11CBRelease(); + owner(None); + CHECK(rec.owner == None); + CHECK(rec.grabN == rec.ungrabN); + for (unsigned int i = 0; i < rec.windowN; ++i) + CHECK(rec.window[i].dead); +} + +static void testTargets(void) +{ + start(); + const unsigned long targets[] = { A_PNG, 999, A_TEXT, A_PNG }; + discover(60, targets, ARRAY_LENGTH(targets)); + CHECK(rec.noticeN == 1); + CHECK(rec.notice[0].count == 2); + CHECK(rec.notice[0].type[0] == LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.notice[0].type[1] == LG_CLIPBOARD_DATA_PNG); + CHECK(rec.destroyN == 1); + CHECK(rec.freeN == 1); + finish(); +} + +static void testNormal(void) +{ + start(); + const unsigned long targets[] = { A_TEXT }; + discover(61, targets, ARRAY_LENGTH(targets)); + const Window window = request(101, LG_CLIPBOARD_DATA_TEXT); + const uint8_t data[] = "normal data"; + prop8(A_TEXT, data, sizeof(data) - 1); + selection(window, A_TEXT, x11atoms.SEL_DATA); + + CHECK(rec.dataN == 1); + CHECK(rec.data[0].request == 101); + CHECK(rec.data[0].type == LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.data[0].size == sizeof(data) - 1); + CHECK(memcmp(rec.data[0].data, data, sizeof(data) - 1) == 0); + CHECK(rec.abortN == 0); + CHECK(rec.destroyN == 2); + CHECK(rec.freeN == 2); + finish(); +} + +static void testIncr(void) +{ + start(); + const unsigned long targets[] = { A_TEXT }; + discover(62, targets, ARRAY_LENGTH(targets)); + const Window window = request(201, LG_CLIPBOARD_DATA_TEXT); + const unsigned long capacity = 2; + prop32(x11atoms.INCR, &capacity, 1); + selection(window, A_TEXT, x11atoms.SEL_DATA); + CHECK(rec.dataN == 0); + CHECK(rec.abortN == 0); + + const uint8_t first[] = "abc"; + const uint8_t second[] = "defg"; + prop8(A_TEXT, first, sizeof(first) - 1); + property(window); + prop8(A_TEXT, second, sizeof(second) - 1); + property(window); + propNull(A_TEXT, 8, 0); + property(window); + + CHECK(rec.dataN == 1); + CHECK(rec.data[0].request == 201); + CHECK(rec.data[0].type == LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.data[0].size == 7); + CHECK(memcmp(rec.data[0].data, "abcdefg", 7) == 0); + CHECK(rec.abortN == 0); + CHECK(rec.destroyN == 2); + CHECK(rec.freeN == 4); + finish(); +} + +static void testMalformed(void) +{ + start(); + const unsigned long targets[] = { A_TEXT }; + discover(63, targets, ARRAY_LENGTH(targets)); + + Window window = request(301, LG_CLIPBOARD_DATA_TEXT); + const unsigned long bad = 1; + prop32(A_TEXT, &bad, 1); + selection(window, A_TEXT, x11atoms.SEL_DATA); + CHECK(rec.abortN == 1); + CHECK(rec.abort[0] == 301); + + window = request(302, LG_CLIPBOARD_DATA_TEXT); + const unsigned long capacity = 8; + prop32(x11atoms.INCR, &capacity, 1); + selection(window, A_TEXT, x11atoms.SEL_DATA); + prop32(A_TEXT, &bad, 1); + property(window); + CHECK(rec.abortN == 2); + CHECK(rec.abort[1] == 302); + + window = request(303, LG_CLIPBOARD_DATA_TEXT); + propNull(A_TEXT, 8, 3); + selection(window, A_TEXT, x11atoms.SEL_DATA); + CHECK(rec.abortN == 3); + CHECK(rec.abort[2] == 303); + CHECK(rec.dataN == 0); + finish(); +} + +static void testReplace(void) +{ + start(); + const unsigned long targets[] = { A_TEXT }; + discover(64, targets, ARRAY_LENGTH(targets)); + const Window first = request(401, LG_CLIPBOARD_DATA_TEXT); + + const unsigned int convertN = rec.convertN; + owner(65); + CHECK(rec.convertN == convertN + 1); + CHECK(rec.abortN == 1); + CHECK(rec.abort[0] == 401); + CHECK(rec.window[1].window == first); + CHECK(rec.window[1].dead); + + const Window second = request(402, LG_CLIPBOARD_DATA_TEXT); + owner(None); + CHECK(rec.abortN == 2); + CHECK(rec.abort[1] == 402); + CHECK(rec.releaseN == 1); + bool dead = false; + for (unsigned int i = 0; i < rec.windowN; ++i) + if (rec.window[i].window == second) + dead = rec.window[i].dead; + CHECK(dead); + + x11CBRequest(403, LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.abortN == 3); + CHECK(rec.abort[2] == 403); + finish(); +} + +static void testSource(void) +{ + start(); + x11CBNotice(LG_CLIPBOARD_DATA_PNG); + CHECK(rec.owner == x11.window); + + selectionRequest(x11atoms.TARGETS, 700); + CHECK(rec.changeN == 1); + CHECK(rec.change[0].window == 900); + CHECK(rec.change[0].property == 700); + CHECK(rec.change[0].type == XA_ATOM); + CHECK(rec.change[0].format == 32); + CHECK(rec.change[0].count == 2); + const Atom * values = (const Atom *)rec.change[0].data; + CHECK(values[0] == x11atoms.TARGETS); + CHECK(values[1] == A_PNG); + CHECK(rec.sendN == 1); + CHECK(rec.send[0].xselection.property == 700); + + selectionRequest(A_PNG, 701); + CHECK(rec.requestN == 1); + CHECK(rec.requestType == LG_CLIPBOARD_DATA_PNG); + CHECK(rec.reply); + CHECK(rec.sendN == 1); + const uint8_t data[] = { 1, 2, 3, 4 }; + rec.reply(rec.replyOpaque, LG_CLIPBOARD_DATA_PNG, + data, ARRAY_LENGTH(data)); + CHECK(rec.changeN == 2); + CHECK(rec.change[1].property == 701); + CHECK(rec.change[1].type == A_PNG); + CHECK(rec.change[1].format == 8); + CHECK(rec.change[1].count == 4); + CHECK(memcmp(rec.change[1].data, data, sizeof(data)) == 0); + CHECK(rec.sendN == 2); + CHECK(rec.send[1].xselection.property == 701); + + selectionRequest(A_TEXT, 702); + CHECK(rec.requestN == 1); + CHECK(rec.sendN == 3); + CHECK(rec.send[2].xselection.property == None); + + rec.requestOK = false; + selectionRequest(A_PNG, 703); + CHECK(rec.requestN == 2); + CHECK(rec.sendN == 4); + CHECK(rec.send[3].xselection.property == None); + + x11CBRelease(); + CHECK(rec.owner == None); + finish(); +} + +static void testTeardown(void) +{ + start(); + const unsigned int convertN = rec.convertN; + owner(66); + CHECK(rec.convertN == convertN + 1); + const Window targets = rec.convert[convertN].requestor; + const Window read = request(501, LG_CLIPBOARD_DATA_TEXT); + x11CBNotice(LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.abortN == 1); + CHECK(rec.abort[0] == 501); + CHECK(rec.owner == x11.window); + CHECK(rec.destroyN == 2); + + bool targetsDead = false; + bool readDead = false; + for (unsigned int i = 0; i < rec.windowN; ++i) + { + if (rec.window[i].window == targets) + targetsDead = rec.window[i].dead; + if (rec.window[i].window == read) + readDead = rec.window[i].dead; + } + CHECK(targetsDead); + CHECK(readDead); + + x11CBRelease(); + CHECK(rec.owner == None); + finish(); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "targets" , testTargets }, + { "normal" , testNormal }, + { "incr" , testIncr }, + { "malformed", testMalformed }, + { "replace" , testReplace }, + { "source" , testSource }, + { "teardown" , testTeardown }, +}; + +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; +}