phase 1: unfirehose reconstruction from session JSONL ingest
Source: ~/.unfirehose/unfirehose.db (project_id=81, 4 sessions covering
2026-03-29 through 2026-04-05). Reconstructed via chronological replay
of Write/Edit tool_input on file_paths under /home/fox/zebra-report/.
stats:
files reconstructed: 20
writes baselined: all (zero missing)
edits applied: 68
edits unapplied: 8 (1 SKIP pre-baseline, 6 FAIL old_string drift, 1 AMBIGUOUS)
unapplied edits represent small drift in 6 files; baseline content for
each is intact. quality verification deferred to phase 2.
recovered tree:
CLAUDE.md, Makefile
src/{tx,rx,pulse,carrier,chat,bt}.c
include/{modem,zebra}.h
test/{functional,integration,unit}.c, test/test.h
web/{index,kernel}.html, web/blog/style.css
blog/build.py, blog/posts/{001-volume-modem,002-sse-chatroom}.md
report: /tmp/zebra_recover_report.txt
script: /tmp/zebra_recover.py
This commit is contained in:
commit
b77da42bbe
20 changed files with 4062 additions and 0 deletions
319
test/functional.c
Normal file
319
test/functional.c
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
/*
|
||||
* functional.c — end-to-end TX/RX pipeline and auto-negotiate tests
|
||||
*
|
||||
* Requires two live PulseAudio sink-inputs:
|
||||
* ZEBRA_DATA_SINK — TX modulates this, RX reads it (data channel)
|
||||
* ZEBRA_CTRL_SINK — RX modulates this, TX reads it (handshake channel)
|
||||
*
|
||||
* Run ./tx -l to find sink indices, then:
|
||||
* ZEBRA_DATA_SINK=15815 ZEBRA_CTRL_SINK=15923 ./test/functional
|
||||
*
|
||||
* Compile: gcc -Wall -O2 -Iinclude $(pkg-config --cflags libpulse) \
|
||||
* -o test/functional test/functional.c src/pulse.c \
|
||||
* $(pkg-config --libs libpulse) -lrt -lpthread
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include <semaphore.h>
|
||||
#include <time.h>
|
||||
#include "zebra.h"
|
||||
#include "modem.h"
|
||||
#include "test.h"
|
||||
|
||||
static uint32_t data_sink = 0;
|
||||
static uint32_t ctrl_sink = 0;
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* shared RX byte collector *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
#define COLLECT_MAX 256
|
||||
|
||||
typedef struct {
|
||||
char buf[COLLECT_MAX];
|
||||
int len;
|
||||
int want; /* stop after this many bytes */
|
||||
sem_t done;
|
||||
} collector_t;
|
||||
|
||||
static void collect_byte(uint8_t b, void *ud) {
|
||||
collector_t *c = ud;
|
||||
if (c->len < COLLECT_MAX - 1) c->buf[c->len++] = (char)b;
|
||||
if (c->len >= c->want) sem_post(&c->done);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* test 1: fixed-baud loopback *
|
||||
* TX and RX both run against the same data_sink at 50 baud. *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
zebra_pulse_t *z;
|
||||
uint32_t sink;
|
||||
uint8_t channels;
|
||||
int baud;
|
||||
const char *msg;
|
||||
} tx_arg_t;
|
||||
|
||||
typedef struct {
|
||||
zebra_pulse_t *z;
|
||||
uint32_t sink;
|
||||
int baud;
|
||||
collector_t *col;
|
||||
} rx_arg_t;
|
||||
|
||||
static void *tx_thread(void *arg) {
|
||||
tx_arg_t *a = arg;
|
||||
/* brief delay so RX loop is running before TX fires */
|
||||
struct timespec d = {0, 200000000L}; /* 200ms */
|
||||
nanosleep(&d, NULL);
|
||||
|
||||
long period_ns = 1000000000L / a->baud;
|
||||
zebra_set_volume_fast(a->z, a->sink, a->channels, ZEBRA_VOL_MARK);
|
||||
|
||||
struct timespec next;
|
||||
clock_gettime(CLOCK_MONOTONIC, &next);
|
||||
ts_add_ns(&next, period_ns);
|
||||
|
||||
for (int i = 0; a->msg[i]; i++)
|
||||
zebra_send_byte(a->z, a->sink, a->channels, (uint8_t)a->msg[i], &next, period_ns);
|
||||
|
||||
zebra_set_volume_fast(a->z, a->sink, a->channels, ZEBRA_VOL_MARK);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *rx_thread(void *arg) {
|
||||
rx_arg_t *a = arg;
|
||||
zebra_rx_run(a->z, a->sink, a->baud, collect_byte, a->col);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void t_loopback_fixed_baud(void) {
|
||||
const char *msg = "ZEBRA\n";
|
||||
const int baud = 50;
|
||||
const int timeout_s = 10;
|
||||
|
||||
collector_t col;
|
||||
memset(&col, 0, sizeof(col));
|
||||
col.want = (int)strlen(msg);
|
||||
sem_init(&col.done, 0, 0);
|
||||
|
||||
zebra_pulse_t ztx, zrx;
|
||||
T_EQ(zebra_pulse_connect(&ztx, "zebra-test-tx"), 0);
|
||||
T_EQ(zebra_pulse_connect(&zrx, "zebra-test-rx"), 0);
|
||||
|
||||
/* resolve channel count for TX */
|
||||
uint8_t ch = 2;
|
||||
{
|
||||
zebra_sink_t buf[64];
|
||||
int n = zebra_list_sinks(&ztx, buf, 64);
|
||||
for (int i = 0; i < n; i++)
|
||||
if (buf[i].index == data_sink)
|
||||
{ ch = buf[i].channels ? buf[i].channels : 2; break; }
|
||||
}
|
||||
|
||||
tx_arg_t txa = { .z = &ztx, .sink = data_sink, .channels = ch,
|
||||
.baud = baud, .msg = msg };
|
||||
rx_arg_t rxa = { .z = &zrx, .sink = data_sink, .baud = baud, .col = &col };
|
||||
|
||||
pthread_t tx_tid, rx_tid;
|
||||
pthread_create(&rx_tid, NULL, rx_thread, &rxa);
|
||||
pthread_create(&tx_tid, NULL, tx_thread, &txa);
|
||||
|
||||
/* wait for collector or timeout */
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += timeout_s;
|
||||
int timed_out = sem_timedwait(&col.done, &deadline);
|
||||
|
||||
pthread_cancel(rx_tid);
|
||||
pthread_detach(rx_tid);
|
||||
pthread_join(tx_tid, NULL);
|
||||
|
||||
T_EQ(timed_out, 0); /* did not time out */
|
||||
col.buf[col.len] = '\0';
|
||||
T_EQ(memcmp(col.buf, msg, strlen(msg)), 0);
|
||||
fprintf(stderr, " [rx] received %d bytes: %.*s", col.len, col.len, col.buf);
|
||||
|
||||
zebra_pulse_disconnect(&ztx);
|
||||
zebra_pulse_disconnect(&zrx);
|
||||
sem_destroy(&col.done);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* test 2: auto-negotiate handshake *
|
||||
* RX benchmarks, sends offer on ctrl_sink. *
|
||||
* TX receives offer, then sends data on data_sink at negotiated baud. *
|
||||
* RX receives data at negotiated baud. *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
zebra_pulse_t *z_data; /* for TX data */
|
||||
zebra_pulse_t *z_ctrl; /* for reading handshake */
|
||||
uint32_t data_sink;
|
||||
uint32_t ctrl_sink;
|
||||
uint8_t data_ch;
|
||||
const char *msg;
|
||||
int timeout_ms;
|
||||
} autoneg_tx_arg_t;
|
||||
|
||||
typedef struct {
|
||||
zebra_pulse_t *z_data; /* for RX data */
|
||||
zebra_pulse_t *z_ctrl; /* for sending handshake */
|
||||
uint32_t data_sink;
|
||||
uint32_t ctrl_sink;
|
||||
uint8_t ctrl_ch;
|
||||
collector_t *col;
|
||||
} autoneg_rx_arg_t;
|
||||
|
||||
static void *autoneg_tx_thread(void *arg) {
|
||||
autoneg_tx_arg_t *a = arg;
|
||||
uint16_t neg_baud = 0;
|
||||
fprintf(stderr, " [tx] waiting for handshake on ctrl_sink %u...\n", a->ctrl_sink);
|
||||
if (zebra_recv_handshake(a->z_ctrl, a->ctrl_sink, a->timeout_ms, &neg_baud) < 0) {
|
||||
fprintf(stderr, " [tx] handshake timeout — no frame received\n");
|
||||
return NULL;
|
||||
}
|
||||
fprintf(stderr, " [tx] OFFER received: %u baud — waiting for READY\n", neg_baud);
|
||||
if (zebra_recv_ready(a->z_ctrl, a->ctrl_sink, ZEBRA_HS_READY_WAIT) < 0) {
|
||||
fprintf(stderr, " [tx] READY timeout — aborting\n");
|
||||
return NULL;
|
||||
}
|
||||
fprintf(stderr, " [tx] READY received — sending '%s' at %u baud on data_sink %u\n",
|
||||
a->msg, neg_baud, a->data_sink);
|
||||
long period_ns = 1000000000L / neg_baud;
|
||||
zebra_set_volume_fast(a->z_data, a->data_sink, a->data_ch, ZEBRA_VOL_MARK);
|
||||
|
||||
struct timespec next;
|
||||
clock_gettime(CLOCK_MONOTONIC, &next);
|
||||
ts_add_ns(&next, period_ns);
|
||||
|
||||
for (int i = 0; a->msg[i]; i++)
|
||||
zebra_send_byte(a->z_data, a->data_sink, a->data_ch,
|
||||
(uint8_t)a->msg[i], &next, period_ns);
|
||||
|
||||
zebra_set_volume_fast(a->z_data, a->data_sink, a->data_ch, ZEBRA_VOL_MARK);
|
||||
fprintf(stderr, " [tx] send complete\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *autoneg_rx_thread(void *arg) {
|
||||
autoneg_rx_arg_t *a = arg;
|
||||
int baud = zebra_benchmark_baud(a->z_data, a->data_sink);
|
||||
fprintf(stderr, " [rx] benchmark: %d baud\n", baud);
|
||||
fprintf(stderr, " [rx] sending handshake on ctrl_sink %u ch=%u\n",
|
||||
a->ctrl_sink, a->ctrl_ch);
|
||||
zebra_send_handshake(a->z_ctrl, a->ctrl_sink, a->ctrl_ch, (uint16_t)baud);
|
||||
fprintf(stderr, " [rx] OFFER sent — sending READY (3x) then entering receive loop\n");
|
||||
zebra_send_ready(a->z_ctrl, a->ctrl_sink, a->ctrl_ch);
|
||||
|
||||
fprintf(stderr, " [rx] entering receive loop at %d baud on data_sink %u\n",
|
||||
baud, a->data_sink);
|
||||
zebra_rx_run(a->z_data, a->data_sink, baud, collect_byte, a->col);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void t_autoneg_pipeline(void) {
|
||||
const char *msg = "PING\n";
|
||||
const int timeout_s = 30;
|
||||
const int timeout_ms = 15000; /* handshake timeout for TX */
|
||||
|
||||
collector_t col;
|
||||
memset(&col, 0, sizeof(col));
|
||||
col.want = (int)strlen(msg);
|
||||
sem_init(&col.done, 0, 0);
|
||||
|
||||
/* four connections: TX-data, TX-ctrl, RX-data, RX-ctrl */
|
||||
zebra_pulse_t z_tx_data, z_tx_ctrl, z_rx_data, z_rx_ctrl;
|
||||
T_EQ(zebra_pulse_connect(&z_tx_data, "zebra-test-tx-data"), 0);
|
||||
T_EQ(zebra_pulse_connect(&z_tx_ctrl, "zebra-test-tx-ctrl"), 0);
|
||||
T_EQ(zebra_pulse_connect(&z_rx_data, "zebra-test-rx-data"), 0);
|
||||
T_EQ(zebra_pulse_connect(&z_rx_ctrl, "zebra-test-rx-ctrl"), 0);
|
||||
|
||||
/* resolve channel counts */
|
||||
uint8_t data_ch = 2, ctrl_ch = 2;
|
||||
{
|
||||
zebra_sink_t buf[64];
|
||||
int n = zebra_list_sinks(&z_tx_data, buf, 64);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (buf[i].index == data_sink) data_ch = buf[i].channels ? buf[i].channels : 2;
|
||||
if (buf[i].index == ctrl_sink) ctrl_ch = buf[i].channels ? buf[i].channels : 2;
|
||||
}
|
||||
}
|
||||
|
||||
autoneg_tx_arg_t txa = {
|
||||
.z_data = &z_tx_data, .z_ctrl = &z_tx_ctrl,
|
||||
.data_sink = data_sink, .ctrl_sink = ctrl_sink,
|
||||
.data_ch = data_ch, .msg = msg, .timeout_ms = timeout_ms
|
||||
};
|
||||
autoneg_rx_arg_t rxa = {
|
||||
.z_data = &z_rx_data, .z_ctrl = &z_rx_ctrl,
|
||||
.data_sink = data_sink, .ctrl_sink = ctrl_sink,
|
||||
.ctrl_ch = ctrl_ch, .col = &col
|
||||
};
|
||||
|
||||
pthread_t tx_tid, rx_tid;
|
||||
pthread_create(&tx_tid, NULL, autoneg_tx_thread, &txa);
|
||||
pthread_create(&rx_tid, NULL, autoneg_rx_thread, &rxa);
|
||||
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += timeout_s;
|
||||
int timed_out = sem_timedwait(&col.done, &deadline);
|
||||
|
||||
pthread_cancel(rx_tid);
|
||||
pthread_detach(rx_tid); /* don't join — PA wait may not respond to cancel */
|
||||
pthread_join(tx_tid, NULL);
|
||||
|
||||
T_EQ(timed_out, 0);
|
||||
col.buf[col.len] = '\0';
|
||||
T_EQ(memcmp(col.buf, msg, strlen(msg)), 0);
|
||||
fprintf(stderr, " [rx] received %d bytes: %.*s", col.len, col.len, col.buf);
|
||||
|
||||
zebra_pulse_disconnect(&z_tx_data);
|
||||
zebra_pulse_disconnect(&z_tx_ctrl);
|
||||
zebra_pulse_disconnect(&z_rx_data);
|
||||
zebra_pulse_disconnect(&z_rx_ctrl);
|
||||
sem_destroy(&col.done);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* main *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
int main(void) {
|
||||
const char *d = getenv("ZEBRA_DATA_SINK");
|
||||
const char *c = getenv("ZEBRA_CTRL_SINK");
|
||||
|
||||
if (!d || !c) {
|
||||
fprintf(stderr,
|
||||
"ZEBRA_DATA_SINK and ZEBRA_CTRL_SINK must be set.\n"
|
||||
" run: ./tx -l to list available sink indices\n"
|
||||
" example: ZEBRA_DATA_SINK=15815 ZEBRA_CTRL_SINK=15923 ./test/functional\n"
|
||||
"\n"
|
||||
" DATA_SINK: TX modulates it, RX reads it (e.g. 'zebra report' Firefox tab)\n"
|
||||
" CTRL_SINK: RX modulates it, TX reads it (e.g. 'X' Firefox tab)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
data_sink = (uint32_t)atoi(d);
|
||||
ctrl_sink = (uint32_t)atoi(c);
|
||||
|
||||
if (data_sink == ctrl_sink) {
|
||||
fprintf(stderr, "error: DATA_SINK and CTRL_SINK must be different\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "data_sink=%u ctrl_sink=%u\n\n", data_sink, ctrl_sink);
|
||||
|
||||
T_SECTION("loopback: fixed baud (50 baud)");
|
||||
T_RUN(t_loopback_fixed_baud);
|
||||
|
||||
T_SECTION("auto-negotiate pipeline");
|
||||
T_RUN(t_autoneg_pipeline);
|
||||
|
||||
T_SUMMARY();
|
||||
}
|
||||
152
test/integration.c
Normal file
152
test/integration.c
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* integration.c — PulseAudio connection and volume I/O tests
|
||||
*
|
||||
* Requires a live PulseAudio / PipeWire session.
|
||||
* Set ZEBRA_TEST_SINK to a sink-input index (from ./tx -l) before running.
|
||||
* If unset, PA connection tests still run; volume tests are skipped.
|
||||
*
|
||||
* Compile: gcc -Wall -O2 -Iinclude $(pkg-config --cflags libpulse) \
|
||||
* -o test/integration test/integration.c src/pulse.c \
|
||||
* $(pkg-config --libs libpulse) -lrt -lpthread
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "zebra.h"
|
||||
#include "modem.h"
|
||||
#include "test.h"
|
||||
|
||||
static zebra_pulse_t z;
|
||||
static uint32_t test_sink = (uint32_t)-1;
|
||||
static int have_sink = 0;
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* PA connection *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void t_connect(void) {
|
||||
int r = zebra_pulse_connect(&z, "zebra-test");
|
||||
T_EQ(r, 0);
|
||||
}
|
||||
|
||||
static void t_list_sinks(void) {
|
||||
zebra_sink_t buf[64];
|
||||
int n = zebra_list_sinks(&z, buf, 64);
|
||||
T_GE(n, 0);
|
||||
if (n > 0) {
|
||||
/* every entry must have a valid channel count */
|
||||
for (int i = 0; i < n; i++)
|
||||
T_GE((int)buf[i].channels, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void t_find_sink_miss(void) {
|
||||
zebra_sink_t s;
|
||||
int r = zebra_find_sink(&z, "____no_such_sink_xyzzy____", &s);
|
||||
T_EQ(r, -1);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* volume I/O (needs ZEBRA_TEST_SINK) *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void t_get_volume(void) {
|
||||
if (!have_sink) { fprintf(stderr, " (skip — ZEBRA_TEST_SINK not set)\n"); return; }
|
||||
uint8_t vol = 255;
|
||||
int r = zebra_get_volume(&z, test_sink, &vol);
|
||||
T_EQ(r, 0);
|
||||
T_LE((int)vol, 100);
|
||||
}
|
||||
|
||||
static void t_set_get_roundtrip(void) {
|
||||
if (!have_sink) { fprintf(stderr, " (skip — ZEBRA_TEST_SINK not set)\n"); return; }
|
||||
uint8_t original = 255;
|
||||
zebra_get_volume(&z, test_sink, &original);
|
||||
|
||||
/* set to MARK, read back */
|
||||
zebra_set_volume(&z, test_sink, ZEBRA_VOL_MARK);
|
||||
uint8_t got = 0;
|
||||
int r = zebra_get_volume(&z, test_sink, &got);
|
||||
T_EQ(r, 0);
|
||||
/* PA quantises to PA_VOLUME_NORM steps — allow ±2% tolerance */
|
||||
T_GE((int)got, (int)ZEBRA_VOL_MARK - 2);
|
||||
T_LE((int)got, (int)ZEBRA_VOL_MARK + 2);
|
||||
|
||||
/* restore */
|
||||
zebra_set_volume(&z, test_sink, original);
|
||||
}
|
||||
|
||||
static void t_set_get_space(void) {
|
||||
if (!have_sink) { fprintf(stderr, " (skip — ZEBRA_TEST_SINK not set)\n"); return; }
|
||||
uint8_t original = 255;
|
||||
zebra_get_volume(&z, test_sink, &original);
|
||||
|
||||
zebra_set_volume(&z, test_sink, ZEBRA_VOL_SPACE);
|
||||
uint8_t got = 0;
|
||||
zebra_get_volume(&z, test_sink, &got);
|
||||
T_GE((int)got, (int)ZEBRA_VOL_SPACE - 2);
|
||||
T_LE((int)got, (int)ZEBRA_VOL_SPACE + 2);
|
||||
|
||||
zebra_set_volume(&z, test_sink, original);
|
||||
}
|
||||
|
||||
static void t_vol_clamp_over_100(void) {
|
||||
if (!have_sink) { fprintf(stderr, " (skip — ZEBRA_TEST_SINK not set)\n"); return; }
|
||||
uint8_t original = 255;
|
||||
zebra_get_volume(&z, test_sink, &original);
|
||||
|
||||
/* pct > 100 should be clamped, not crash */
|
||||
int r = zebra_set_volume(&z, test_sink, 200);
|
||||
T_EQ(r, 0);
|
||||
uint8_t got = 0;
|
||||
zebra_get_volume(&z, test_sink, &got);
|
||||
T_LE((int)got, 100);
|
||||
|
||||
zebra_set_volume(&z, test_sink, original);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* benchmark sanity *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void t_benchmark_range(void) {
|
||||
if (!have_sink) { fprintf(stderr, " (skip — ZEBRA_TEST_SINK not set)\n"); return; }
|
||||
int baud = zebra_benchmark_baud(&z, test_sink);
|
||||
T_GE(baud, ZEBRA_BAUD_MIN);
|
||||
T_LE(baud, ZEBRA_BAUD_MAX);
|
||||
fprintf(stderr, " measured: %d baud\n", baud);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* main *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
int main(void) {
|
||||
const char *sink_env = getenv("ZEBRA_TEST_SINK");
|
||||
if (sink_env) {
|
||||
test_sink = (uint32_t)atoi(sink_env);
|
||||
have_sink = 1;
|
||||
fprintf(stderr, "ZEBRA_TEST_SINK=%u\n", test_sink);
|
||||
} else {
|
||||
fprintf(stderr, "ZEBRA_TEST_SINK not set — volume tests will skip\n");
|
||||
fprintf(stderr, " run: ./tx -l to list sinks, then set the env var\n");
|
||||
}
|
||||
|
||||
T_SECTION("PA connection");
|
||||
T_RUN(t_connect);
|
||||
T_RUN(t_list_sinks);
|
||||
T_RUN(t_find_sink_miss);
|
||||
|
||||
T_SECTION("volume I/O");
|
||||
T_RUN(t_get_volume);
|
||||
T_RUN(t_set_get_roundtrip);
|
||||
T_RUN(t_set_get_space);
|
||||
T_RUN(t_vol_clamp_over_100);
|
||||
|
||||
T_SECTION("benchmark");
|
||||
T_RUN(t_benchmark_range);
|
||||
|
||||
zebra_pulse_disconnect(&z);
|
||||
T_SUMMARY();
|
||||
}
|
||||
37
test/test.h
Normal file
37
test/test.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static int _t_pass = 0, _t_fail = 0;
|
||||
static const char *_t_current = "";
|
||||
|
||||
#define T_RUN(fn) do { \
|
||||
_t_current = #fn; \
|
||||
int _before = _t_fail; \
|
||||
fn(); \
|
||||
fprintf(stderr, " %-48s %s\n", #fn, (_t_fail == _before) ? "ok" : "FAIL"); \
|
||||
} while(0)
|
||||
|
||||
#define T_CHECK(expr) do { \
|
||||
if (expr) { _t_pass++; } \
|
||||
else { _t_fail++; \
|
||||
fprintf(stderr, " check failed: %s (%s:%d)\n", #expr, __FILE__, __LINE__); } \
|
||||
} while(0)
|
||||
|
||||
#define T_EQ(a,b) T_CHECK((a) == (b))
|
||||
#define T_NEQ(a,b) T_CHECK((a) != (b))
|
||||
#define T_GE(a,b) T_CHECK((a) >= (b))
|
||||
#define T_LE(a,b) T_CHECK((a) <= (b))
|
||||
#define T_GT(a,b) T_CHECK((a) > (b))
|
||||
|
||||
#define T_SUMMARY() do { \
|
||||
fprintf(stderr, "\n%d passed %d failed\n", _t_pass, _t_fail); \
|
||||
return _t_fail ? 1 : 0; \
|
||||
} while(0)
|
||||
|
||||
#define T_SECTION(name) fprintf(stderr, "\n%s\n", name)
|
||||
|
||||
#define T_SKIP(reason) do { \
|
||||
fprintf(stderr, " SKIP: %s\n", reason); \
|
||||
return 0; \
|
||||
} while(0)
|
||||
275
test/unit.c
Normal file
275
test/unit.c
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
* unit.c — pure logic tests, no PulseAudio required
|
||||
*
|
||||
* Tests: signal encoding, timing arithmetic, baud math, handshake frame.
|
||||
* Compile: gcc -Wall -O2 -Iinclude $(pkg-config --cflags libpulse) -o test/unit test/unit.c
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "zebra.h"
|
||||
#include "modem.h"
|
||||
#include "test.h"
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* signal encoding *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void t_bit_to_vol(void) {
|
||||
T_EQ(zebra_bit_to_vol(0), ZEBRA_VOL_SPACE);
|
||||
T_EQ(zebra_bit_to_vol(1), ZEBRA_VOL_MARK);
|
||||
}
|
||||
|
||||
static void t_vol_to_bit(void) {
|
||||
T_EQ(zebra_vol_to_bit(ZEBRA_VOL_SPACE), 0);
|
||||
T_EQ(zebra_vol_to_bit(ZEBRA_VOL_MARK), 1);
|
||||
T_EQ(zebra_vol_to_bit(0), 0);
|
||||
T_EQ(zebra_vol_to_bit(100), 1);
|
||||
T_EQ(zebra_vol_to_bit(ZEBRA_VOL_THRESHOLD), -1);
|
||||
T_EQ(zebra_vol_to_bit(ZEBRA_VOL_THRESHOLD - 1), 0);
|
||||
T_EQ(zebra_vol_to_bit(ZEBRA_VOL_THRESHOLD + 1), 1);
|
||||
}
|
||||
|
||||
static void t_signal_roundtrip(void) {
|
||||
T_EQ(zebra_vol_to_bit(zebra_bit_to_vol(0)), 0);
|
||||
T_EQ(zebra_vol_to_bit(zebra_bit_to_vol(1)), 1);
|
||||
}
|
||||
|
||||
static void t_signal_separation(void) {
|
||||
/* MARK and SPACE must be on opposite sides of threshold */
|
||||
T_GT((int)ZEBRA_VOL_MARK, (int)ZEBRA_VOL_THRESHOLD);
|
||||
T_GT((int)ZEBRA_VOL_THRESHOLD, (int)ZEBRA_VOL_SPACE);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* timing arithmetic *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void t_ts_add_basic(void) {
|
||||
struct timespec ts = {1, 0};
|
||||
ts_add_ns(&ts, 500000000L);
|
||||
T_EQ(ts.tv_sec, 1);
|
||||
T_EQ(ts.tv_nsec, 500000000L);
|
||||
}
|
||||
|
||||
static void t_ts_add_overflow(void) {
|
||||
struct timespec ts = {1, 800000000L};
|
||||
ts_add_ns(&ts, 400000000L); /* 800M + 400M = 1200M → carry */
|
||||
T_EQ(ts.tv_sec, 2);
|
||||
T_EQ(ts.tv_nsec, 200000000L);
|
||||
}
|
||||
|
||||
static void t_ts_add_multi_second(void) {
|
||||
struct timespec ts = {0, 0};
|
||||
ts_add_ns(&ts, 3500000000L); /* 3.5 seconds */
|
||||
T_EQ(ts.tv_sec, 3);
|
||||
T_EQ(ts.tv_nsec, 500000000L);
|
||||
}
|
||||
|
||||
static void t_ts_add_zero(void) {
|
||||
struct timespec ts = {5, 123456789L};
|
||||
ts_add_ns(&ts, 0);
|
||||
T_EQ(ts.tv_sec, 5);
|
||||
T_EQ(ts.tv_nsec, 123456789L);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* baud math *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void t_baud_from_avg_ns(void) {
|
||||
/* 400µs poll → 500 baud (4x oversample: 2e8/400000) */
|
||||
T_EQ(zebra_baud_from_avg_ns(400000L), 500);
|
||||
/* 200µs poll → 1000 baud */
|
||||
T_EQ(zebra_baud_from_avg_ns(200000L), 1000);
|
||||
/* 1ms poll → 200 baud */
|
||||
T_EQ(zebra_baud_from_avg_ns(1000000L), 200);
|
||||
}
|
||||
|
||||
static void t_baud_clamp_min(void) {
|
||||
/* extremely slow: result clamped to ZEBRA_BAUD_MIN */
|
||||
T_EQ(zebra_baud_from_avg_ns(1000000000L), ZEBRA_BAUD_MIN);
|
||||
}
|
||||
|
||||
static void t_baud_clamp_max(void) {
|
||||
/* extremely fast: result clamped to ZEBRA_BAUD_MAX */
|
||||
T_EQ(zebra_baud_from_avg_ns(1L), ZEBRA_BAUD_MAX);
|
||||
}
|
||||
|
||||
static void t_baud_zero_guard(void) {
|
||||
/* zero avg_ns returns safe default */
|
||||
T_EQ(zebra_baud_from_avg_ns(0L), ZEBRA_BAUD_DEFAULT);
|
||||
}
|
||||
|
||||
static void t_baud_range(void) {
|
||||
/* result always in valid range for a sweep of latencies */
|
||||
long ns[] = {1, 100, 1000, 10000, 100000, 400000, 1000000, 10000000L, 100000000L};
|
||||
for (int i = 0; i < 9; i++) {
|
||||
int b = zebra_baud_from_avg_ns(ns[i]);
|
||||
T_GE(b, ZEBRA_BAUD_MIN);
|
||||
T_LE(b, ZEBRA_BAUD_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* handshake frame *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void t_hs_build_magic(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
zebra_hs_build(frame, 1000);
|
||||
T_EQ(frame[0], ZEBRA_HS_MAGIC_0);
|
||||
T_EQ(frame[1], ZEBRA_HS_MAGIC_1);
|
||||
T_EQ(frame[2], ZEBRA_HS_TYPE_OFFER);
|
||||
}
|
||||
|
||||
static void t_hs_build_baud_encoding(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
zebra_hs_build(frame, 0x0342); /* 834 decimal */
|
||||
T_EQ(frame[3], 0x42); /* low byte */
|
||||
T_EQ(frame[4], 0x03); /* high byte */
|
||||
}
|
||||
|
||||
static void t_hs_build_checksum(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
zebra_hs_build(frame, 1072);
|
||||
uint8_t ck = frame[0] ^ frame[1] ^ frame[2] ^ frame[3] ^ frame[4];
|
||||
T_EQ(frame[5], ck);
|
||||
}
|
||||
|
||||
static void t_hs_parse_valid(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
uint16_t baud = 0;
|
||||
zebra_hs_build(frame, 1072);
|
||||
T_EQ(zebra_hs_parse(frame, &baud), 0);
|
||||
T_EQ(baud, 1072);
|
||||
}
|
||||
|
||||
static void t_hs_roundtrip(void) {
|
||||
uint16_t bauds[] = {1, 10, 100, 500, 1000, 1072, 2000, 9999, 65535};
|
||||
for (int i = 0; i < 9; i++) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
uint16_t got = 0;
|
||||
zebra_hs_build(frame, bauds[i]);
|
||||
T_EQ(zebra_hs_parse(frame, &got), 0);
|
||||
T_EQ(got, bauds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void t_hs_parse_bad_magic0(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
uint16_t baud = 0;
|
||||
zebra_hs_build(frame, 100);
|
||||
frame[0] ^= 0xFF; /* corrupt first magic byte */
|
||||
T_EQ(zebra_hs_parse(frame, &baud), -1);
|
||||
}
|
||||
|
||||
static void t_hs_parse_bad_magic1(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
uint16_t baud = 0;
|
||||
zebra_hs_build(frame, 100);
|
||||
frame[1] ^= 0xFF;
|
||||
T_EQ(zebra_hs_parse(frame, &baud), -1);
|
||||
}
|
||||
|
||||
static void t_hs_parse_bad_type(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
uint16_t baud = 0;
|
||||
zebra_hs_build(frame, 100);
|
||||
frame[2] = 0xFF; /* unknown type */
|
||||
T_EQ(zebra_hs_parse(frame, &baud), -1);
|
||||
}
|
||||
|
||||
static void t_hs_parse_bad_checksum(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
uint16_t baud = 0;
|
||||
zebra_hs_build(frame, 100);
|
||||
frame[5] ^= 0x01; /* flip one checksum bit */
|
||||
T_EQ(zebra_hs_parse(frame, &baud), -1);
|
||||
}
|
||||
|
||||
static void t_hs_parse_corrupt_baud(void) {
|
||||
uint8_t frame[ZEBRA_HS_FRAME_LEN];
|
||||
uint16_t baud = 0;
|
||||
zebra_hs_build(frame, 100);
|
||||
frame[3] ^= 0x01; /* corrupt baud byte without fixing checksum */
|
||||
T_EQ(zebra_hs_parse(frame, &baud), -1);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* READY frame *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void t_hs_build_ready_fields(void) {
|
||||
uint8_t f[ZEBRA_HS_FRAME_LEN];
|
||||
zebra_hs_build_ready(f);
|
||||
T_EQ(f[0], ZEBRA_HS_MAGIC_0);
|
||||
T_EQ(f[1], ZEBRA_HS_MAGIC_1);
|
||||
T_EQ(f[2], ZEBRA_HS_TYPE_READY);
|
||||
T_EQ(f[3], 0);
|
||||
T_EQ(f[4], 0);
|
||||
}
|
||||
|
||||
static void t_hs_ready_checksum(void) {
|
||||
uint8_t f[ZEBRA_HS_FRAME_LEN];
|
||||
zebra_hs_build_ready(f);
|
||||
uint8_t ck = f[0] ^ f[1] ^ f[2] ^ f[3] ^ f[4];
|
||||
T_EQ(f[5], ck);
|
||||
}
|
||||
|
||||
static void t_hs_ready_not_offer(void) {
|
||||
/* READY frame must NOT parse as OFFER (type guard) */
|
||||
uint8_t f[ZEBRA_HS_FRAME_LEN];
|
||||
uint16_t baud = 0;
|
||||
zebra_hs_build_ready(f);
|
||||
T_EQ(zebra_hs_parse(f, &baud), -1);
|
||||
}
|
||||
|
||||
static void t_hs_ready_distinct_from_offer(void) {
|
||||
/* READY and OFFER frames must differ in the type byte */
|
||||
T_NEQ(ZEBRA_HS_TYPE_READY, ZEBRA_HS_TYPE_OFFER);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* main *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
int main(void) {
|
||||
T_SECTION("signal encoding");
|
||||
T_RUN(t_bit_to_vol);
|
||||
T_RUN(t_vol_to_bit);
|
||||
T_RUN(t_signal_roundtrip);
|
||||
T_RUN(t_signal_separation);
|
||||
|
||||
T_SECTION("timing arithmetic");
|
||||
T_RUN(t_ts_add_basic);
|
||||
T_RUN(t_ts_add_overflow);
|
||||
T_RUN(t_ts_add_multi_second);
|
||||
T_RUN(t_ts_add_zero);
|
||||
|
||||
T_SECTION("baud math");
|
||||
T_RUN(t_baud_from_avg_ns);
|
||||
T_RUN(t_baud_clamp_min);
|
||||
T_RUN(t_baud_clamp_max);
|
||||
T_RUN(t_baud_zero_guard);
|
||||
T_RUN(t_baud_range);
|
||||
|
||||
T_SECTION("handshake frame: OFFER");
|
||||
T_RUN(t_hs_build_magic);
|
||||
T_RUN(t_hs_build_baud_encoding);
|
||||
T_RUN(t_hs_build_checksum);
|
||||
T_RUN(t_hs_parse_valid);
|
||||
T_RUN(t_hs_roundtrip);
|
||||
T_RUN(t_hs_parse_bad_magic0);
|
||||
T_RUN(t_hs_parse_bad_magic1);
|
||||
T_RUN(t_hs_parse_bad_type);
|
||||
T_RUN(t_hs_parse_bad_checksum);
|
||||
T_RUN(t_hs_parse_corrupt_baud);
|
||||
|
||||
T_SECTION("handshake frame: READY");
|
||||
T_RUN(t_hs_build_ready_fields);
|
||||
T_RUN(t_hs_ready_checksum);
|
||||
T_RUN(t_hs_ready_not_offer);
|
||||
T_RUN(t_hs_ready_distinct_from_offer);
|
||||
|
||||
T_SUMMARY();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue