OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid / esp324xlight / test / grants_test.cpp

290 lines · cpp ref:
// Host tests for grants.h — the shared-control engine — with a fake clock.
//   g++ -std=c++17 -Wall -Wextra -I.. grants_test.cpp -o grants_test && ./grants_test
#include "grants.h"

#include <stdio.h>
#include <string.h>

using namespace cth;

static int failures = 0, checks = 0;
#define CHECK(cond) do { checks++; if (!(cond)) { failures++; \
  printf("FAIL %s:%d  %s\n", __FILE__, __LINE__, #cond); } } while (0)

static Engine fresh(uint32_t epoch = 7) {
  Engine e;
  e.begin(epoch, true);
  return e;
}

static int drain(Engine &e, Event *out, int max) {
  int n = 0;
  Event ev;
  while (e.popEvent(ev)) if (n < max) out[n++] = ev;
  return n;
}

static void test_first_come() {
  Engine e = fresh();
  int busy;
  CHECK(e.grant(1, 0, "alice", 1000, busy) == Engine::Granted);
  CHECK(e.grant(1, 1, "bob", 1001, busy) == Engine::Busy);
  CHECK(busy == 1);
  CHECK(e.grant(2, 1, "bob", 1002, busy) == Engine::Granted);   // other slot: fine
  const char *who; uint32_t n;
  CHECK(e.held(1, who, n) && strcmp(who, "alice") == 0 && n == 1);
  CHECK(e.held(2, who, n) && strcmp(who, "bob") == 0 && n == 2);
  Event ev[8];
  int k = drain(e, ev, 8);
  CHECK(k == 2 && ev[0].kind == Event::Held && ev[0].slot == 1 && ev[0].epoch == 7);
}

static void test_grant_all_is_atomic() {
  Engine e = fresh();
  int busy;
  CHECK(e.grant(2, 1, "bob", 0, busy) == Engine::Granted);
  CHECK(e.grant(ALL, 0, "alice", 0, busy) == Engine::Busy);
  CHECK(busy == 2);
  const char *who; uint32_t n;
  CHECK(!e.held(0, who, n));          // nothing taken by the failed `all`
  CHECK(e.release(2, 1, 0));
  CHECK(e.grant(ALL, 0, "alice", 0, busy) == Engine::Granted);
  for (int s = 0; s < SLOTS; s++) CHECK(e.held(s, who, n) && strcmp(who, "alice") == 0);
}

static void test_set_needs_holder_and_grant() {
  Engine e = fresh();
  int busy;
  e.grant(0, 3, "alice", 0, busy);
  const char *who; uint32_t n;
  e.held(0, who, n);
  CHECK(e.set(0, 4, 7, n, 1, 2, 3, 0, 10) == Engine::NotHeld);        // wrong socket
  CHECK(e.set(0, 3, 7, n + 1, 1, 2, 3, 0, 10) == Engine::NotHeld);    // wrong grant
  CHECK(e.set(0, 3, 6, n, 1, 2, 3, 0, 10) == Engine::NotHeld);        // wrong epoch
  CHECK(e.set(0, 3, 7, n, 1, 2, RED_MAX + 1, 0, 10) == Engine::BadValue);
  CHECK(e.set(0, 3, 7, n, 100, 200, 300, 0, 10) == Engine::SetOk);
  CHECK(e.value(0) == 100 && e.value(1) == 200 && e.value(2) == 300);
  CHECK(e.set(1, 3, 7, n, 1, 1, 1, 0, 10) == Engine::NotHeld);        // slot not held
}

static void test_idle_expiry_reports_final_value() {
  Engine e = fresh();
  int busy;
  e.grant(3, 0, "alice", 0, busy);
  const char *who; uint32_t n;
  e.held(3, who, n);
  e.set(3, 0, 7, n, 10, 20, 30, 0, 1500);   // movement resets the idle clock
  e.tick(3400);
  CHECK(e.held(3, who, n));                 // 1900 ms since the last move
  e.tick(3500);
  CHECK(!e.held(3, who, n));                // 2000 ms: freed
  Event ev[8];
  int k = drain(e, ev, 8);
  CHECK(k == 2);
  CHECK(ev[1].kind == Event::Free && ev[1].why == Why::Idle && ev[1].slot == 3);
  CHECK(ev[1].b == 10 && ev[1].g == 20 && ev[1].r == 30 && ev[1].n == n);
  CHECK(strcmp(ev[1].holder, "alice") == 0);
}

static void test_max_expiry_even_while_moving() {
  Engine e = fresh();
  int busy;
  e.grant(0, 0, "alice", 0, busy);
  const char *who; uint32_t n;
  e.held(0, who, n);
  uint32_t t = 0;
  for (; t < MAX_MS; t += 500) { e.set(0, 0, 7, n, t % 1000, 0, 0, 0, t); e.tick(t); }
  e.tick(MAX_MS);
  CHECK(!e.held(0, who, n));
  Event ev[80];
  int k = drain(e, ev, 80);
  CHECK(ev[k - 1].kind == Event::Free && ev[k - 1].why == Why::Max);
  // A re-grab can't extend a drag: re-granting a held slot keeps its clock.
  Engine f = fresh();
  f.grant(0, 0, "alice", 0, busy);
  f.grant(0, 0, "alice", 29000, busy);
  f.tick(29500);
  f.grant(0, 0, "alice", 29600, busy);   // re-grab mid-drag
  f.tick(30000);
  CHECK(!f.held(0, who, n));
}

static void test_release_and_disconnect() {
  Engine e = fresh();
  int busy;
  e.grant(0, 0, "alice", 0, busy);
  e.grant(1, 1, "bob", 0, busy);
  CHECK(!e.release(0, 1, 5));             // bob can't release alice's slot
  CHECK(e.release(0, 0, 5));
  e.socketClosed(1);
  const char *who; uint32_t n;
  CHECK(!e.held(0, who, n) && !e.held(1, who, n));
  Event ev[8];
  int k = drain(e, ev, 8);
  CHECK(k == 4 && ev[2].why == Why::Release && ev[3].why == Why::Disconnect);
}

static void test_stopall_overrides_drags() {
  Engine e = fresh();
  int busy;
  e.grant(ALL, 0, "alice", 0, busy);
  e.stopAll();
  const char *who; uint32_t n;
  for (int s = 0; s < SLOTS; s++) CHECK(!e.held(s, who, n));
  Event ev[16];
  int k = drain(e, ev, 16);
  CHECK(k == 8 && ev[7].why == Why::Blackout);
}

static void test_legacy_skips_held_slots() {
  Engine e = fresh();
  int busy;
  e.grant(1, 0, "alice", 0, busy);
  uint32_t v[CHANNELS];
  for (int c = 0; c < CHANNELS; c++) v[c] = 1000 + c;
  CHECK(e.legacy(v, CHANNELS, 0, 0) == 9);
  CHECK(e.value(0) == 1000 && e.value(3) == 0 && e.value(5) == 0 && e.value(6) == 1006);
}

static void test_per_channel_fades_independent() {
  Engine e = fresh();
  int busy;
  e.grant(0, 0, "alice", 0, busy);
  e.grant(1, 1, "bob", 0, busy);
  const char *who; uint32_t na, nb;
  e.held(0, who, na);
  e.held(1, who, nb);
  e.set(0, 0, 7, na, 0, 0, 40000, 1000, 0);    // alice fades slot 0 over 1 s
  e.tick(500);
  e.set(1, 1, 7, nb, 30000, 0, 0, 0, 500);     // bob writes slot 1 mid-fade
  e.tick(500);
  CHECK(e.value(2) > 15000 && e.value(2) < 25000);   // alice's fade continued (linear, gamma 1)
  CHECK(e.value(3) == 30000);
  e.tick(1000);
  CHECK(e.value(2) == 40000);
  CHECK(!e.anyFading());
}

static void test_fade_reports_target_on_release() {
  Engine e = fresh();
  int busy;
  e.grant(0, 0, "alice", 0, busy);
  const char *who; uint32_t n;
  e.held(0, who, n);
  e.set(0, 0, 7, n, 60000, 0, 0, 5000, 0);
  e.tick(100);
  e.release(0, 0, 100);
  Event ev[4];
  int k = drain(e, ev, 4);
  CHECK(ev[k - 1].b == 60000);   // ends on the target, not the mid-fade value
}

static void test_wraparound() {
  Engine e = fresh();
  int busy;
  uint32_t t0 = 0xFFFFFF00u;               // just before millis() wraps
  e.grant(0, 0, "alice", t0, busy);
  const char *who; uint32_t n;
  e.tick(t0 + 1000);                       // wrapped past zero
  CHECK(e.held(0, who, n));
  e.tick(t0 + 2000);
  CHECK(!e.held(0, who, n));
}

static void test_disabled_without_epoch() {
  Engine e;
  e.begin(0, false);
  int busy;
  CHECK(e.grant(0, 0, "alice", 0, busy) == Engine::Disabled);
  uint32_t v[3] = {1, 2, 3};
  CHECK(e.legacy(v, 3, 0, 0) == 3);        // legacy control still works
}

static void test_values_broadcast_pacing() {
  Engine e = fresh();
  uint32_t v[CHANNELS] = {0};
  v[0] = 5;
  e.legacy(v, CHANNELS, 0, 0);
  CHECK(e.valuesDue(0));
  v[0] = 6;
  e.legacy(v, CHANNELS, 0, 100);
  CHECK(!e.valuesDue(100));                // inside the 250 ms window
  CHECK(e.valuesDue(250));
  CHECK(!e.valuesDue(600));                // nothing changed since
}

static void test_free_message_format() {
  Event ev;
  memset(&ev, 0, sizeof(ev));
  ev.kind = Event::Free;
  ev.slot = 2;
  ev.epoch = 9;
  ev.n = 41;
  ev.b = 1; ev.g = 2; ev.r = 45000;
  char buf[160];
  freeMessage(buf, sizeof(buf), "studiolight", ev);
  CHECK(strcmp(buf, "cthuloid/free/1 studiolight 2 9 41 1 2 45000") == 0);
}

static void test_pick_slot() {
  bool occ[3] = {true, true, true};
  uint32_t last[3] = {100, 50, 10};
  bool holds[3] = {false, false, true};
  CHECK(pickSlot(occ, last, holds, 3, 200) == 1);   // oldest WITHOUT a grant
  occ[0] = false;
  CHECK(pickSlot(occ, last, holds, 3, 200) == 0);   // a free slot first
  occ[0] = true;
  bool all[3] = {true, true, true};
  CHECK(pickSlot(occ, last, all, 3, 200) == -1);    // everyone holds: refuse
}

static void test_set_epoch_live() {
  // epochfloor recovery: a reflashed unit raises its epoch while running. Grants
  // under the old epoch end (blackout semantics), numbering restarts under the new.
  Engine e = fresh(3);
  int busy;
  e.grant(0, 0, "alice", 0, busy);
  e.setEpoch(12);
  const char *who; uint32_t n;
  CHECK(!e.held(0, who, n));
  CHECK(e.epoch() == 12);
  Event ev[4];
  int k = drain(e, ev, 4);
  CHECK(k == 2 && ev[1].kind == Event::Free && ev[1].epoch == 3);   // freed under the OLD epoch
  e.grant(0, 0, "alice", 0, busy);
  e.held(0, who, n);
  CHECK(n == 1);
  k = drain(e, ev, 4);
  CHECK(k == 1 && ev[0].epoch == 12);
}

static void test_rate_cap() {
  RateCap r;
  int ok = 0;
  for (int i = 0; i < 100; i++) ok += r.allow(500);
  CHECK(ok == 60 && r.dropped == 40);
  CHECK(r.allow(1500));                    // new window
}

int main() {
  test_first_come();
  test_grant_all_is_atomic();
  test_set_needs_holder_and_grant();
  test_idle_expiry_reports_final_value();
  test_max_expiry_even_while_moving();
  test_release_and_disconnect();
  test_stopall_overrides_drags();
  test_legacy_skips_held_slots();
  test_per_channel_fades_independent();
  test_fade_reports_target_on_release();
  test_wraparound();
  test_disabled_without_epoch();
  test_values_broadcast_pacing();
  test_free_message_format();
  test_pick_slot();
  test_set_epoch_live();
  test_rate_cap();
  printf("%d/%d checks passed\n", checks - failures, checks);
  return failures ? 1 : 0;
}