Files

407 lines
11 KiB
C

/*
* timer subsystem tests.
*
* these exercise the binary min-heap in tmr.c without involving the
* multiplexer or any device. time is supplied explicitly to
* hio_firetmrjobs() so every case is deterministic and instantaneous.
*
* the invariant that matters most here is the idxptr contract: every
* scheduled job writes its current heap position into *idxptr on every
* sift, so a caller holding a hio_tmridx_t always has a usable index for
* hio_deltmrjob()/hio_updtmrjob() no matter how the heap has been
* reshuffled since insertion.
*/
#include <hio-prv.h>
#include "tap.h"
#define MAX_LOG 64
static hio_t* g_hio = HIO_NULL;
static int g_log[MAX_LOG];
static int g_log_len = 0;
static void log_reset (void)
{
g_log_len = 0;
}
static int log_matches (const int* expected, int n)
{
int i;
if (g_log_len != n) return 0;
for (i = 0; i < n; i++)
{
if (g_log[i] != expected[i]) return 0;
}
return 1;
}
static void h_record (hio_t* hio, const hio_ntime_t* now, hio_tmrjob_t* job)
{
if (g_log_len < MAX_LOG) g_log[g_log_len++] = (int)(hio_uintptr_t)job->ctx;
}
/* schedule a job firing at 'secs' with a tag recoverable through job->ctx */
static hio_tmridx_t sched_at (int secs, int tag, hio_tmridx_t* idxptr, hio_tmrjob_handler_t h)
{
hio_tmrjob_t job;
HIO_MEMSET (&job, 0, HIO_SIZEOF(job));
job.ctx = (void*)(hio_uintptr_t)tag;
HIO_INIT_NTIME (&job.when, secs, 0);
job.handler = h? h: h_record;
job.idxptr = idxptr;
return hio_instmrjob(g_hio, &job);
}
static void fire_at (int secs)
{
hio_ntime_t now;
HIO_INIT_NTIME (&now, secs, 0);
hio_firetmrjobs (g_hio, &now, HIO_NULL);
}
/* ------------------------------------------------------------------ */
static void test_ordering (void)
{
static const int expected[] = { 1, 2, 3, 4, 5 };
hio_tmridx_t idx[5];
int i;
log_reset ();
/* insert out of order on purpose */
sched_at (5, 5, &idx[0], HIO_NULL);
sched_at (1, 1, &idx[1], HIO_NULL);
sched_at (4, 4, &idx[2], HIO_NULL);
sched_at (2, 2, &idx[3], HIO_NULL);
sched_at (3, 3, &idx[4], HIO_NULL);
OK (g_hio->tmr.size == 5, "5 jobs scheduled");
/* nothing is due yet at t=0 */
fire_at (0);
OK (g_log_len == 0, "no job fires before its deadline");
/* partially due */
fire_at (2);
OK (g_log_len == 2, "only the two due jobs fire at t=2");
fire_at (10);
OK (log_matches(expected, 5), "jobs fire in deadline order regardless of insertion order");
OK (g_hio->tmr.size == 0, "heap is empty after all jobs fire");
for (i = 0; i < 5; i++)
{
if (idx[i] != HIO_TMRIDX_INVALID) break;
}
OK (i == 5, "every idxptr is invalidated once its job has fired");
}
static void test_idxptr_tracking (void)
{
/* the heap reshuffles aggressively during inserts. after every insert
* each live job's recorded index must still resolve back to that job. */
enum { N = 24 };
hio_tmridx_t idx[N];
int deadlines[N];
int i, bad;
hio_cleartmrjobs (g_hio);
log_reset ();
/* a deliberately awkward deadline sequence - not sorted, with duplicates */
for (i = 0; i < N; i++) deadlines[i] = ((i * 7) % 11) + 1;
bad = 0;
for (i = 0; i < N; i++)
{
int j;
sched_at (deadlines[i], i, &idx[i], HIO_NULL);
/* every previously inserted job must still be reachable at its recorded index */
for (j = 0; j <= i; j++)
{
hio_tmrjob_t* job = hio_gettmrjob(g_hio, idx[j]);
if (!job || (int)(hio_uintptr_t)job->ctx != j) { bad++; break; }
}
}
OK (bad == 0, "idxptr stays correct for every live job across N inserts");
/* now delete every third job and re-verify the survivors */
bad = 0;
for (i = 0; i < N; i += 3)
{
hio_deltmrjob (g_hio, idx[i]);
if (idx[i] != HIO_TMRIDX_INVALID) bad++;
}
OK (bad == 0, "hio_deltmrjob() invalidates the caller's index holder");
bad = 0;
for (i = 0; i < N; i++)
{
hio_tmrjob_t* job;
if (i % 3 == 0) continue; /* deleted above */
job = hio_gettmrjob(g_hio, idx[i]);
if (!job || (int)(hio_uintptr_t)job->ctx != i) bad++;
}
OK (bad == 0, "idxptr stays correct for surviving jobs after interleaved deletes");
/* the surviving jobs must still come out in deadline order */
fire_at (100);
bad = 0;
for (i = 1; i < g_log_len; i++)
{
if (deadlines[g_log[i - 1]] > deadlines[g_log[i]]) bad++;
}
OK (bad == 0, "surviving jobs still fire in deadline order");
OK (g_log_len == N - ((N + 2) / 3), "exactly the undeleted jobs fired");
}
static void test_cancel (void)
{
hio_tmridx_t a, b, c;
static const int expected[] = { 1, 3 };
hio_cleartmrjobs (g_hio);
log_reset ();
sched_at (1, 1, &a, HIO_NULL);
sched_at (2, 2, &b, HIO_NULL);
sched_at (3, 3, &c, HIO_NULL);
/* cancel through the *current* index, which is what idxptr holds */
hio_deltmrjob (g_hio, b);
OK (b == HIO_TMRIDX_INVALID, "cancelled job's index holder is invalidated");
OK (g_hio->tmr.size == 2, "heap shrank by exactly one");
fire_at (10);
OK (log_matches(expected, 2), "a cancelled job never fires");
}
static void test_update (void)
{
hio_tmridx_t a, b;
hio_tmrjob_t job;
static const int expected[] = { 2 };
hio_cleartmrjobs (g_hio);
log_reset ();
sched_at (10, 1, &a, HIO_NULL);
sched_at (20, 2, &b, HIO_NULL);
/* push job 1 out past job 2 */
HIO_MEMSET (&job, 0, HIO_SIZEOF(job));
job.ctx = (void*)(hio_uintptr_t)1;
HIO_INIT_NTIME (&job.when, 30, 0);
job.handler = h_record;
job.idxptr = &a;
hio_updtmrjob (g_hio, a, &job);
OK (hio_gettmrjob(g_hio, a) != HIO_NULL, "index holder is still valid after update");
OK ((int)(hio_uintptr_t)hio_gettmrjob(g_hio, a)->ctx == 1, "index holder still points at the same job");
fire_at (25);
OK (log_matches(expected, 1), "a job pushed later does not fire at its old deadline");
fire_at (35);
OK (g_log_len == 2 && g_log[1] == 1, "the rescheduled job fires at its new deadline");
/* and the other direction: pull a job earlier */
hio_cleartmrjobs (g_hio);
log_reset ();
sched_at (10, 1, &a, HIO_NULL);
sched_at (20, 2, &b, HIO_NULL);
HIO_MEMSET (&job, 0, HIO_SIZEOF(job));
job.ctx = (void*)(hio_uintptr_t)2;
HIO_INIT_NTIME (&job.when, 5, 0);
job.handler = h_record;
job.idxptr = &b;
hio_updtmrjob (g_hio, b, &job);
fire_at (7);
OK (g_log_len == 1 && g_log[0] == 2, "a job pulled earlier fires at its new deadline");
}
/* ------------------------------------------------------------------ */
static hio_tmridx_t g_victim_idx;
static void h_cancel_other (hio_t* hio, const hio_ntime_t* now, hio_tmrjob_t* job)
{
h_record (hio, now, job);
/* cancelling a still-pending job from inside a firing handler. the heap
* is mid-drain here, so this is the ordering most likely to corrupt it. */
if (g_victim_idx != HIO_TMRIDX_INVALID) hio_deltmrjob (hio, g_victim_idx);
}
static void test_cancel_during_fire (void)
{
hio_tmridx_t a, c;
static const int expected[] = { 1, 3 };
hio_cleartmrjobs (g_hio);
log_reset ();
sched_at (1, 1, &a, h_cancel_other);
sched_at (2, 2, &g_victim_idx, HIO_NULL);
sched_at (3, 3, &c, HIO_NULL);
fire_at (10);
OK (log_matches(expected, 2), "a job cancelled from inside a firing handler never fires");
OK (g_victim_idx == HIO_TMRIDX_INVALID, "the cancelled job's holder is invalidated mid-drain");
OK (g_hio->tmr.size == 0, "heap is consistent and empty after cancel-during-fire");
}
/* ------------------------------------------------------------------ */
static int g_rearm_left = 3;
static hio_tmridx_t g_rearm_idx;
static void h_rearm (hio_t* hio, const hio_ntime_t* now, hio_tmrjob_t* job)
{
h_record (hio, now, job);
if (--g_rearm_left > 0)
{
/* re-arm from within our own handler. by this point the heap has
* already removed us, so this is a plain insert. */
hio_tmrjob_t nj;
HIO_MEMSET (&nj, 0, HIO_SIZEOF(nj));
nj.ctx = job->ctx;
nj.when = *now;
nj.when.sec += 1;
nj.handler = h_rearm;
nj.idxptr = &g_rearm_idx;
hio_instmrjob (hio, &nj);
}
}
static void test_rearm_during_fire (void)
{
hio_cleartmrjobs (g_hio);
log_reset ();
g_rearm_left = 3;
g_rearm_idx = HIO_TMRIDX_INVALID;
sched_at (1, 9, &g_rearm_idx, h_rearm);
/* firing at t=1 must not drain the re-armed job scheduled for t=2 */
fire_at (1);
OK (g_log_len == 1, "a handler re-arming itself does not re-fire in the same drain");
OK (g_hio->tmr.size == 1, "the re-armed job is pending");
fire_at (2);
OK (g_log_len == 2, "the re-armed job fires at its new deadline");
fire_at (3);
OK (g_log_len == 3, "re-arming works repeatedly");
OK (g_hio->tmr.size == 0, "the handler stopped re-arming and the heap drained");
}
/* ------------------------------------------------------------------ */
static void test_tmout (void)
{
hio_ntime_t now, tmout;
hio_tmridx_t a, b;
hio_cleartmrjobs (g_hio);
log_reset ();
OK (hio_gettmrtmout(g_hio, HIO_NULL, &tmout) == 0, "no timeout is reported with an empty heap");
sched_at (30, 1, &a, HIO_NULL);
sched_at (10, 2, &b, HIO_NULL);
HIO_INIT_NTIME (&now, 4, 0);
OK (hio_gettmrtmout(g_hio, &now, &tmout) == 1, "a timeout is reported when jobs are pending");
OK (tmout.sec == 6 && tmout.nsec == 0, "the timeout is measured to the earliest deadline");
/* an overdue job must clamp to zero rather than going negative */
HIO_INIT_NTIME (&now, 50, 0);
hio_gettmrtmout (g_hio, &now, &tmout);
OK (tmout.sec == 0 && tmout.nsec == 0, "an overdue deadline clamps the timeout to zero");
hio_cleartmrjobs (g_hio);
OK (g_hio->tmr.size == 0, "hio_cleartmrjobs() empties the heap");
OK (a == HIO_TMRIDX_INVALID && b == HIO_TMRIDX_INVALID, "hio_cleartmrjobs() invalidates every index holder");
}
/* ------------------------------------------------------------------ */
static void test_growth (void)
{
/* the heap array starts at capacity 1 and doubles. walk well past
* several reallocations and confirm ordering survives them. */
enum { N = 200 };
hio_tmridx_t idx[N];
int i, bad;
hio_cleartmrjobs (g_hio);
log_reset ();
for (i = 0; i < N; i++) sched_at (N - i, i, &idx[i], HIO_NULL);
OK (g_hio->tmr.size == N, "heap grew to hold every job");
bad = 0;
for (i = 0; i < N; i++)
{
hio_tmrjob_t* job = hio_gettmrjob(g_hio, idx[i]);
if (!job || (int)(hio_uintptr_t)job->ctx != i) bad++;
}
OK (bad == 0, "idxptr survives heap reallocation");
/* drain in slices so the log stays inside MAX_LOG */
bad = 0;
for (i = 1; i <= N; i++)
{
log_reset ();
fire_at (i);
if (g_log_len != 1 || g_log[0] != N - i) bad++;
}
OK (bad == 0, "200 jobs drain one per tick in exact deadline order");
OK (g_hio->tmr.size == 0, "heap fully drained");
}
/* ------------------------------------------------------------------ */
/* keep the library's own debug logging out of the test output; genuine
* errors still surface on stderr where the TAP driver captures them */
static void quiet_logging (hio_t* hio)
{
hio_bitmask_t mask = HIO_LOG_ERROR | HIO_LOG_FATAL | HIO_LOG_ALL_TYPES;
hio_setoption (hio, HIO_LOG_MASK, &mask);
}
int main (void)
{
hio_errinf_t errinf;
no_plan ();
/* the timer needs no multiplexer, so open with logging only. tmrcapa of
* 1 forces the growth path to be exercised from the very first insert. */
g_hio = hio_open(HIO_NULL, 0, HIO_NULL, HIO_FEATURE_LOG, 1, &errinf);
quiet_logging (g_hio);
if (!g_hio)
{
bail_out ("unable to open hio");
return -1;
}
test_ordering ();
test_idxptr_tracking ();
test_cancel ();
test_update ();
test_cancel_during_fire ();
test_rearm_during_fire ();
test_tmout ();
test_growth ();
hio_close (g_hio);
return exit_status();
}