Files
LookingGlass/client/tests/frame_timing_test.c
2026-08-13 04:28:09 +10:00

801 lines
22 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 "test.h"
#define main lgClientMain
#include "../src/main.c"
#undef main
#include <math.h>
#include <pthread.h>
#define TEST_ALARM_S 5U
struct Feedback
{
unsigned int count;
uint64_t serial;
uint32_t generation;
uint32_t epoch;
uint32_t deadline;
uint64_t phase;
};
static struct Feedback feed;
static unsigned int rawConnectCalls;
static unsigned int cancellableConnectCalls;
static bool connectSawCancellation;
uint64_t renderQueue_sourceBegin(RenderQueueSource source)
{
(void)source;
return 1;
}
void renderQueue_sourceInvalidate(RenderQueueSource source,
uint64_t generation)
{
(void)source;
(void)generation;
}
void renderQueue_sourceClearCursor(RenderQueueSource source)
{
(void)source;
}
bool renderQueue_sourceSwSurfaceConfigure(RenderQueueSource source,
uint64_t generation, int width, int height)
{
(void)source;
(void)generation;
(void)width;
(void)height;
return true;
}
void renderQueue_sourceSwSurfaceDrawFill(RenderQueueSource source,
uint64_t generation, int x, int y, int width, int height,
uint32_t color)
{
(void)source;
(void)generation;
(void)x;
(void)y;
(void)width;
(void)height;
(void)color;
}
void renderQueue_sourceSwSurfaceDrawBitmap(RenderQueueSource source,
uint64_t generation, int x, int y, int width, int height, int stride,
const void * data, bool topDown)
{
(void)source;
(void)generation;
(void)x;
(void)y;
(void)width;
(void)height;
(void)stride;
(void)data;
(void)topDown;
}
void renderQueue_sourceCursorState(RenderQueueSource source,
uint64_t generation, bool visible, int x, int y, int hx, int hy)
{
(void)source;
(void)generation;
(void)visible;
(void)x;
(void)y;
(void)hx;
(void)hy;
}
void renderQueue_sourceCursorImage(RenderQueueSource source,
uint64_t generation, LG_RendererCursor type,
int width, int height, int pitch, const void * data)
{
(void)source;
(void)generation;
(void)type;
(void)width;
(void)height;
(void)pitch;
(void)data;
}
void renderQueue_sourceCursorColorTransform(RenderQueueSource source,
uint64_t generation, const LGColorTransform * transform)
{
(void)source;
(void)generation;
(void)transform;
}
void renderQueue_sourceCursorWhiteLevel(RenderQueueSource source,
uint64_t generation, uint32_t sdrWhiteLevel)
{
(void)source;
(void)generation;
(void)sdrWhiteLevel;
}
void overlaySplash_show(bool show)
{
(void)show;
}
bool lgTransportFallback_usable(const LG_TransportFallback * fallback)
{
(void)fallback;
return false;
}
bool lgTransportFallback_ready(const LG_TransportFallback * fallback)
{
(void)fallback;
return false;
}
bool lgTransportFallback_acceptsVideoEvents(
const LG_TransportFallback * fallback)
{
(void)fallback;
return false;
}
bool lgTransportFallback_videoRequested(LG_TransportFallback * fallback)
{
(void)fallback;
return false;
}
void lgTransportFallback_requestVideoActive(
LG_TransportFallback * fallback, bool active)
{
(void)fallback;
(void)active;
}
bool lgInput_available(void)
{
return false;
}
bool core_inputEnabled(void)
{
return false;
}
void core_alignToGuest(void) {}
void core_handleGuestMouseUpdate(void) {}
void core_setTitle(const char * title) { (void)title; }
enum RunState app_getState(void)
{
return atomic_load_explicit(&p_appState, memory_order_acquire);
}
void app_setState(enum RunState state)
{
atomic_store_explicit(&p_appState, state, memory_order_release);
}
void app_invalidateWindow(bool full)
{
(void)full;
}
void app_refreshVideoSource(void) {}
void app_updateMouseState(void) {}
static bool cancelled(void * opaque)
{
(void)opaque;
return true;
}
static LG_TransportStatus rawConnect(LG_Transport * transport,
LG_TransportSession * session)
{
(void)transport;
(void)session;
++rawConnectCalls;
return LG_TRANSPORT_ERROR;
}
static LG_TransportStatus cancellableConnect(LG_Transport * transport,
LG_TransportSession * session, LG_TransportCancelledFn cancelled,
void * opaque)
{
(void)transport;
(void)session;
++cancellableConnectCalls;
connectSawCancellation = cancelled && cancelled(opaque);
return connectSawCancellation ? LG_TRANSPORT_DISCONNECTED :
LG_TRANSPORT_OK;
}
void frameScheduler_feedback(uint64_t frameSerial, uint32_t generation,
uint32_t scheduleEpoch, uint32_t deadlineSerial,
uint64_t measuredPhase)
{
++feed.count;
feed.serial = frameSerial;
feed.generation = generation;
feed.epoch = scheduleEpoch;
feed.deadline = deadlineSerial;
feed.phase = measuredPhase;
}
static LG_TransportFrameTiming srcTiming(bool valid, bool phase)
{
return (LG_TransportFrameTiming)
{
.valid = valid,
.providerValid = true,
.phaseValid = phase,
.scheduleGeneration = 2,
.scheduleEpoch = 3,
.scheduleDeadlineSerial = 4,
.captureTime = 11,
.postProcessTime = 12,
.copyTime = 13,
.readyTime = 14,
.holdTime = 15,
.readyLeadTime = 110,
.receiveTime = 2,
.prepareTime = 3,
};
}
static LG_RendererFrameTiming dstTiming(LG_RendererFrameToken token,
bool tracked)
{
return (LG_RendererFrameTiming)
{
.frameToken = token,
.setupTime = 21,
.effectsTime = 22,
.desktopTime = 23,
.composeTime = 24,
.swapTime = 25,
.presentTracked = tracked,
};
}
static void start(void)
{
memset(&g_state, 0, sizeof(g_state));
memset(&feed, 0, sizeof(feed));
g_state.frameLatency = ringbuffer_new(16, sizeof(OverlayFrameTiming));
CHECK(g_state.frameLatency);
frameTimingInit();
}
static void finish(void)
{
ringbuffer_free(&g_state.frameLatency);
LG_LOCK_FREE(l_frameTiming.lock);
}
static OverlayFrameTiming take(void)
{
OverlayFrameTiming out;
CHECK(ringbuffer_consume(g_state.frameLatency, &out, 1) == 1);
return out;
}
static bool near(float value, float expected)
{
return fabsf(value - expected) < 0.000001f;
}
static void queue(LG_RendererFrameToken token,
const LG_TransportFrameTiming * timing, uint64_t serial)
{
frameTimingQueue(token, serial, timing, 10, 5, 100, 150);
}
static void testLifecycle(void)
{
start();
CHECK(frameTimingQueuedToken() == LG_RENDERER_FRAME_TOKEN_NONE);
const LG_TransportFrameTiming timing = srcTiming(true, true);
const LG_RendererFrameToken first = frameTimingReserve();
CHECK(first == 1);
queue(first, &timing, 10);
CHECK(frameTimingQueuedToken() == first);
CHECK(frameTimingRecord(first)->dispatchTime == 35);
frameTimingCancel(first);
main_framePresented(first, 200, true);
CHECK(frameTimingRecord(first)->token == LG_RENDERER_FRAME_TOKEN_NONE);
const LG_RendererFrameToken second = frameTimingReserve();
CHECK(second == 2);
frameTimingReset();
CHECK(frameTimingQueuedToken() == LG_RENDERER_FRAME_TOKEN_NONE);
CHECK(frameTimingRecord(second)->token == LG_RENDERER_FRAME_TOKEN_NONE);
CHECK(frameTimingReserve() == 3);
finish();
}
static void testOrder(void)
{
start();
const LG_TransportFrameTiming timing = srcTiming(true, true);
const LG_RendererFrameToken token = frameTimingReserve();
queue(token, &timing, 55);
main_framePresented(token, 500, true);
const LG_RendererFrameTiming render = dstTiming(token, true);
frameTimingFinishRender(&render, 200, 7, 300, token);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 0);
frameTimingFinishFrame(token, &timing);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 1);
const OverlayFrameTiming out = take();
CHECK(out.validMask == OVERLAY_FRAME_TIMING_VALID_ALL);
CHECK(near(out.capture, 0.000011f));
CHECK(near(out.transport, 0.000010f));
CHECK(near(out.receive, 0.000002f));
CHECK(near(out.providerPrepare, 0.000003f));
CHECK(near(out.dispatch, 0.000035f));
CHECK(near(out.queue, 0.000050f));
CHECK(near(out.present, 0.000500f));
frameTimingFinishFrame(token, &timing);
main_framePresented(token, 600, true);
frameTimingFinishRender(&render, 200, 7, 300, token);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 0);
finish();
}
static void testUntracked(void)
{
start();
const LG_TransportFrameTiming timing = srcTiming(false, true);
const LG_RendererFrameToken token = frameTimingReserve();
queue(token, &timing, 66);
frameTimingFinishFrame(token, &timing);
const LG_RendererFrameTiming render = dstTiming(token, false);
frameTimingFinishRender(&render, 200, 7, 300, token);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 1);
const OverlayFrameTiming out = take();
CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_PRODUCER));
CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_TRANSPORT));
CHECK((out.validMask & OVERLAY_FRAME_TIMING_VALID_PROVIDER) ==
OVERLAY_FRAME_TIMING_VALID_PROVIDER);
CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_PRESENT));
CHECK(near(out.receive, 0.000002f));
CHECK(near(out.providerPrepare, 0.000003f));
CHECK(out.present == 0.0f);
finish();
}
static void testAccounting(void)
{
start();
LG_TransportFrameTiming timing = srcTiming(true, true);
timing.readyLeadTime = 1;
timing.receiveTime = UINT64_MAX;
timing.prepareTime = UINT64_MAX;
const LG_RendererFrameToken token = frameTimingReserve();
frameTimingQueue(token, 67, &timing, UINT64_MAX, UINT64_MAX, 100, 150);
frameTimingFinishFrame(token, &timing);
const LG_RendererFrameTiming render = dstTiming(token, false);
frameTimingFinishRender(&render, 200, 7, 300, token);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 1);
const OverlayFrameTiming out = take();
CHECK(out.validMask & OVERLAY_FRAME_TIMING_VALID_TRANSPORT);
CHECK((out.validMask & OVERLAY_FRAME_TIMING_VALID_PROVIDER) ==
OVERLAY_FRAME_TIMING_VALID_PROVIDER);
CHECK(out.transport == 0.0f);
CHECK(out.import ==
(float)frameTimingAdd(UINT64_MAX, UINT64_MAX) * 1e-6f);
finish();
}
static void testProviderUnavailable(void)
{
start();
LG_TransportFrameTiming timing = srcTiming(true, true);
timing.providerValid = false;
const LG_RendererFrameToken token = frameTimingReserve();
queue(token, &timing, 68);
frameTimingFinishFrame(token, &timing);
const LG_RendererFrameTiming render = dstTiming(token, false);
frameTimingFinishRender(&render, 200, 7, 300, token);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 1);
const OverlayFrameTiming out = take();
CHECK(out.validMask & OVERLAY_FRAME_TIMING_VALID_PRODUCER);
CHECK(out.validMask & OVERLAY_FRAME_TIMING_VALID_TRANSPORT);
CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_PROVIDER));
CHECK(near(out.transport, 0.000015f));
finish();
}
static void testTimeout(void)
{
start();
const LG_TransportFrameTiming timing = srcTiming(true, true);
const LG_RendererFrameToken token = frameTimingReserve();
queue(token, &timing, 77);
frameTimingFinishFrame(token, &timing);
const LG_RendererFrameTiming render = dstTiming(token, true);
const uint64_t now = nanotime();
frameTimingFinishRender(&render, 200, 7, now, token);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 0);
frameTimingRecord(token)->presentDeadline = nanotime();
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 1);
const OverlayFrameTiming out = take();
CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_PRESENT));
CHECK(out.present == 0.0f);
finish();
}
static void testFifo(void)
{
start();
LG_TransportFrameTiming firstTiming = srcTiming(true, true);
LG_TransportFrameTiming secondTiming = srcTiming(true, true);
const LG_RendererFrameToken first = frameTimingReserve();
const LG_RendererFrameToken second = frameTimingReserve();
firstTiming.captureTime = 81;
secondTiming.captureTime = 82;
queue(first, &firstTiming, 81);
queue(second, &secondTiming, 82);
const LG_RendererFrameTiming r1 = dstTiming(first, false);
const LG_RendererFrameTiming r2 = dstTiming(second, false);
frameTimingFinishRender(&r1, 200, 7, 301, first);
frameTimingFinishRender(&r2, 210, 8, 302, second);
frameTimingFinishFrame(second, &secondTiming);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 0);
frameTimingFinishFrame(first, &firstTiming);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 2);
const OverlayFrameTiming out1 = take();
const OverlayFrameTiming out2 = take();
CHECK(near(out1.capture, 0.000081f));
CHECK(near(out2.capture, 0.000082f));
finish();
}
static void testRetire(void)
{
start();
const LG_TransportFrameTiming timing = srcTiming(true, true);
const LG_RendererFrameToken old = frameTimingReserve();
const LG_RendererFrameToken fresh = frameTimingReserve();
queue(old, &timing, 91);
queue(fresh, &timing, 92);
const LG_RendererFrameTiming render = dstTiming(fresh, false);
frameTimingFinishRender(&render, 200, 7, 300, fresh);
frameTimingFinishFrame(old, &timing);
CHECK(frameTimingRecord(old)->token == LG_RENDERER_FRAME_TOKEN_NONE);
frameTimingFinishFrame(fresh, &timing);
frameTimingPublishReady();
CHECK(ringbuffer_getCount(g_state.frameLatency) == 1);
(void)take();
finish();
}
static void testFeedback(void)
{
start();
g_state.jitRender = true;
const LG_TransportFrameTiming timing = srcTiming(true, true);
LG_RendererFrameToken token = frameTimingReserve();
queue(token, &timing, 101);
LG_RendererFrameTiming render = dstTiming(token, false);
frameTimingFinishRender(&render, 200, 7, 300, token);
CHECK(feed.count == 1);
CHECK(feed.serial == 101);
CHECK(feed.generation == 2);
CHECK(feed.epoch == 3);
CHECK(feed.deadline == 4);
CHECK(feed.phase == 50);
token = frameTimingReserve();
queue(token, &timing, 102);
render = dstTiming(token, false);
frameTimingFinishRender(&render, 200, 7, 300, token - 1);
CHECK(feed.count == 1);
LG_TransportFrameTiming invalid = timing;
invalid.phaseValid = false;
token = frameTimingReserve();
queue(token, &invalid, 103);
render = dstTiming(token, false);
frameTimingFinishRender(&render, 200, 7, 300, token);
CHECK(feed.count == 1);
invalid = timing;
invalid.scheduleEpoch = 0;
token = frameTimingReserve();
queue(token, &invalid, 104);
render = dstTiming(token, false);
frameTimingFinishRender(&render, 200, 7, 300, token);
CHECK(feed.count == 1);
token = frameTimingReserve();
queue(token, &timing, 105);
render = dstTiming(token, false);
frameTimingFinishRender(&render, 149, 7, 300, token);
CHECK(feed.count == 1);
finish();
}
static void testWrap(void)
{
start();
l_frameTiming.nextToken = UINT64_MAX - 1;
CHECK(frameTimingReserve() == UINT64_MAX);
CHECK(frameTimingReserve() == 1);
finish();
}
static void testComponentEpoch(void)
{
atomic_bool available = true;
atomic_uint_least64_t epoch = 7;
atomic_int reason = LG_TRANSPORT_OK;
CHECK(videoComponentPayloadCurrent(&available, &epoch, 7, true));
CHECK(!videoComponentPayloadCurrent(&available, &epoch, 6, true));
CHECK(videoComponentPayloadCurrent(&available, &epoch, 0, false));
const LG_VideoComponentStatus unavailable =
{
.available = false,
.epoch = 8,
.reason = LG_TRANSPORT_ERROR,
};
CHECK(videoComponentStatusChanged(
&available, &epoch, &reason, &unavailable));
CHECK(!videoComponentPayloadCurrent(&available, &epoch, 7, true));
CHECK(!videoComponentPayloadCurrent(&available, &epoch, 0, false));
CHECK(atomic_load(&reason) == LG_TRANSPORT_ERROR);
const LG_VideoComponentStatus replacement =
{
.available = true,
.epoch = 9,
.reason = LG_TRANSPORT_OK,
};
CHECK(videoComponentStatusChanged(
&available, &epoch, &reason, &replacement));
CHECK(!videoComponentPayloadCurrent(&available, &epoch, 7, true));
CHECK(videoComponentPayloadCurrent(&available, &epoch, 9, true));
}
struct PayloadGateTrace
{
const atomic_bool * available;
const atomic_uint_least64_t * epoch;
uint64_t payloadEpoch;
uint64_t sourceGeneration;
atomic_bool entered;
atomic_bool release;
atomic_bool accepted;
atomic_bool changed;
};
static void * holdPayload(void * opaque)
{
struct PayloadGateTrace * trace = opaque;
const bool accepted = videoPayloadAccept(trace->available, trace->epoch,
trace->payloadEpoch, true, trace->sourceGeneration);
atomic_store_explicit(&trace->accepted, accepted, memory_order_release);
if (!accepted)
return NULL;
atomic_store_explicit(&trace->entered, true, memory_order_release);
while (!atomic_load_explicit(&trace->release, memory_order_acquire))
usleep(1000);
videoPayloadRelease();
return NULL;
}
static void * changePayload(void * opaque)
{
struct PayloadGateTrace * trace = opaque;
LG_LOCK(g_state.videoPayloadLock);
atomic_store_explicit((atomic_bool *)trace->available,
false, memory_order_release);
atomic_store_explicit((atomic_uint_least64_t *)trace->epoch,
trace->payloadEpoch + 1, memory_order_release);
LG_UNLOCK(g_state.videoPayloadLock);
atomic_store_explicit(&trace->changed, true, memory_order_release);
return NULL;
}
static void testPayloadGate(void)
{
memset(&g_state, 0, sizeof(g_state));
LG_LOCK_INIT(g_state.videoPayloadLock);
atomic_store_explicit(
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation,
5, memory_order_relaxed);
atomic_bool available = true;
atomic_uint_least64_t epoch = 7;
struct PayloadGateTrace trace =
{
.available = &available,
.epoch = &epoch,
.payloadEpoch = 7,
.sourceGeneration = 5,
};
pthread_t payloadThread;
pthread_t statusThread;
CHECK(pthread_create(&payloadThread, NULL, holdPayload, &trace) == 0);
while (!atomic_load_explicit(&trace.entered, memory_order_acquire))
usleep(1000);
CHECK(pthread_create(&statusThread, NULL, changePayload, &trace) == 0);
usleep(10000);
CHECK(!atomic_load_explicit(&trace.changed, memory_order_acquire));
atomic_store_explicit(&trace.release, true, memory_order_release);
CHECK(pthread_join(payloadThread, NULL) == 0);
CHECK(pthread_join(statusThread, NULL) == 0);
CHECK(atomic_load_explicit(&trace.accepted, memory_order_acquire));
CHECK(atomic_load_explicit(&trace.changed, memory_order_acquire));
CHECK(!videoPayloadAccept(&available, &epoch, 7, true, 5));
LG_LOCK_FREE(g_state.videoPayloadLock);
}
static void testFramePayloadBookkeeping(void)
{
memset(&g_state, 0, sizeof(g_state));
LG_LOCK_INIT(g_state.videoPayloadLock);
atomic_store_explicit(&g_state.videoFrameAvailable,
true, memory_order_relaxed);
atomic_store_explicit(&g_state.videoFrameEpoch, 7, memory_order_relaxed);
atomic_store_explicit(
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation,
5, memory_order_relaxed);
g_state.formatValid = true;
uint64_t sourceGeneration = 4;
uint64_t frameSerial = 41;
uint32_t formatVersion = 3;
bool sourceChanged = false;
LG_TransportFrame frame =
{
.serial = 42,
.epoch = 6,
};
CHECK(!videoFramePayloadAccept(&frame, 5, true,
&sourceGeneration, &frameSerial, &formatVersion, &sourceChanged));
CHECK(sourceGeneration == 4);
CHECK(frameSerial == 41);
CHECK(formatVersion == 3);
frame.epoch = 7;
CHECK(videoFramePayloadAccept(&frame, 5, true,
&sourceGeneration, &frameSerial, &formatVersion, &sourceChanged));
CHECK(sourceChanged);
CHECK(sourceGeneration == 5);
CHECK(frameSerial == 42);
CHECK(formatVersion == 0);
videoPayloadRelease();
CHECK(!videoFramePayloadAccept(&frame, 5, true,
&sourceGeneration, &frameSerial, &formatVersion, &sourceChanged));
LG_LOCK_FREE(g_state.videoPayloadLock);
}
static void testConnectCancellation(void)
{
rawConnectCalls = 0;
cancellableConnectCalls = 0;
connectSawCancellation = false;
const LG_TransportOps ops =
{
.connect = rawConnect,
.connectCancellable = cancellableConnect,
};
struct TransportSessionProbe probe =
{
.ops = &ops,
.cancelled = cancelled,
.done = false,
};
CHECK(transportSessionProbe(&probe) == 0);
CHECK(atomic_load_explicit(&probe.done, memory_order_acquire));
CHECK(probe.status == LG_TRANSPORT_DISCONNECTED);
CHECK(rawConnectCalls == 0);
CHECK(cancellableConnectCalls == 1);
CHECK(connectSawCancellation);
}
static void testRecoveryErrors(void)
{
CHECK(strcmp(recoveryErrorText(LG_RECOVERY_ERR_BUSY),
"A conflicting recovery request is already in progress") == 0);
CHECK(strcmp(recoveryErrorText(LG_RECOVERY_ERR_CAPACITY),
"Too many recovery requests are pending") == 0);
}
struct Test
{
const char * name;
void (*run)(void);
};
static const struct Test tests[] =
{
{ "lifecycle", testLifecycle },
{ "order" , testOrder },
{ "untracked", testUntracked },
{ "accounting", testAccounting },
{ "provider-unavailable", testProviderUnavailable },
{ "timeout" , testTimeout },
{ "fifo" , testFifo },
{ "retire" , testRetire },
{ "feedback" , testFeedback },
{ "wrap" , testWrap },
{ "component-epoch", testComponentEpoch },
{ "payload-gate", testPayloadGate },
{ "frame-payload-bookkeeping", testFramePayloadBookkeeping },
{ "connect-cancel", testConnectCancellation },
{ "recovery-errors", testRecoveryErrors },
};
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 (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;
}