Initial framework: command language, flow layer, host build
A C framework for commanding a Pico WH robot over USB, WiFi or BLE with a
small text DSL. No hardware attached yet, so the whole stack runs on the
host against a simulated HAL and is covered by tests.
Two specifications drive the code:
docs/grammar.md the language — one namespace of callables (native,
firmware-resident, and defined over the wire), procedures
with named arguments and defaults, REPEAT, and the rules
that keep it safe on a microcontroller.
docs/flow.md transport, bracket-balance framing, the envelope,
admission, execution, abort and failsafe.
Three properties the design leans on:
- Bodies may only reference callables that already exist, so the call
graph is acyclic by construction, recursion is unrepresentable, and
interpreter depth is checked at definition time rather than discovered
at runtime.
- Nothing blocks. The interpreter is a resumable state machine over an
explicit frame stack, so ABORT is honoured within one tick even in the
middle of a long move, and no program can overflow the MCU stack.
- No queue. One outstanding statement at a time, which keeps abort to a
single unambiguous victim and avoids inventing an answer to "the
running program failed, does the queued one still go?".
Named arguments are resolved to the callee's parameter order at definition
time, so they cost nothing at execution.
rubo_core is target-independent. Porting to the Pico means providing a
non-blocking rubo_transport_t and a rubo_hal_t, and nothing else.
Not built yet: the Pico transport and HAL, the camera DAT/DEND bulk path,
and IF/ELIF, which is reserved in the grammar and lands with the sensors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hfu1EFVbpee92zvzQunsKb
This commit is contained in:
560
tests/test_rubo.c
Normal file
560
tests/test_rubo.c
Normal file
@@ -0,0 +1,560 @@
|
||||
/* test_rubo.c — host tests for the language, the framer and the executor.
|
||||
*
|
||||
* The emphasis is on the properties the design leans on: the DAG rule, the
|
||||
* depth bound, admission with no queue, and abort. Those are the ones that
|
||||
* stop being theoretical the moment there are motors attached.
|
||||
*/
|
||||
#include "rubo/hal_sim.h"
|
||||
#include "rubo/rubo.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_run, g_fail;
|
||||
static const char *g_case = "";
|
||||
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
g_run++; \
|
||||
if (!(cond)) { \
|
||||
g_fail++; \
|
||||
printf("FAIL [%s] %s:%d: ", g_case, __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* ----------------------------------------------------------- output capture */
|
||||
|
||||
#define MAXL 256
|
||||
static char g_lines[MAXL][256];
|
||||
static int g_nl;
|
||||
|
||||
static void sink(void *ctx, const char *line)
|
||||
{
|
||||
(void)ctx;
|
||||
if (g_nl < MAXL) snprintf(g_lines[g_nl++], sizeof g_lines[0], "%s", line);
|
||||
}
|
||||
|
||||
static void clr(void) { g_nl = 0; }
|
||||
|
||||
static bool saw(const char *sub)
|
||||
{
|
||||
for (int i = 0; i < g_nl; i++)
|
||||
if (strstr(g_lines[i], sub)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void dump(void)
|
||||
{
|
||||
for (int i = 0; i < g_nl; i++) printf(" | %s\n", g_lines[i]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ fixture */
|
||||
|
||||
static uint32_t g_now;
|
||||
|
||||
static rubo_t *mk(void)
|
||||
{
|
||||
rubo_hal_sim_reset();
|
||||
g_now = 1000;
|
||||
clr();
|
||||
rubo_t *r = rubo_create(rubo_hal_sim(), sink, NULL);
|
||||
rubo_note_traffic(r, g_now);
|
||||
rubo_load_stdlib(r, rubo_stdlib);
|
||||
clr();
|
||||
return r;
|
||||
}
|
||||
|
||||
/* Advance time with the link alive. */
|
||||
static void run(rubo_t *r, uint32_t ms)
|
||||
{
|
||||
for (uint32_t i = 0; i < ms; i += 5) {
|
||||
g_now += 5;
|
||||
rubo_note_traffic(r, g_now);
|
||||
rubo_tick(r, g_now);
|
||||
}
|
||||
}
|
||||
|
||||
/* Advance time with nothing arriving — the deadman's domain. */
|
||||
static void run_quiet(rubo_t *r, uint32_t ms)
|
||||
{
|
||||
for (uint32_t i = 0; i < ms; i += 5) {
|
||||
g_now += 5;
|
||||
rubo_tick(r, g_now);
|
||||
}
|
||||
}
|
||||
|
||||
static rubo_err_t sub(rubo_t *r, const char *s)
|
||||
{
|
||||
return rubo_submit(r, s, g_now);
|
||||
}
|
||||
|
||||
/* Submit and run to completion (or until an implausible amount of simulated
|
||||
* time has passed, which counts as a hang). */
|
||||
static rubo_err_t subrun(rubo_t *r, const char *s)
|
||||
{
|
||||
rubo_err_t e = sub(r, s);
|
||||
for (int i = 0; i < 4000 && rubo_state(r) != RUBO_IDLE; i++) {
|
||||
g_now += 5;
|
||||
rubo_note_traffic(r, g_now);
|
||||
rubo_tick(r, g_now);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
#define CASE(name) do { g_case = (name); clr(); } while (0)
|
||||
|
||||
/* ------------------------------------------------------------------- tests */
|
||||
|
||||
static void test_basics(void)
|
||||
{
|
||||
rubo_t *r = mk();
|
||||
|
||||
CASE("stdlib loaded");
|
||||
CHECK(sub(r, "STAT") == RUBO_OK, "STAT failed");
|
||||
CHECK(saw("proc=9"), "expected 6 natives + 3 stdlib procedures");
|
||||
|
||||
CASE("control words bypass the language");
|
||||
clr();
|
||||
CHECK(sub(r, "PING") == RUBO_OK, "PING failed");
|
||||
CHECK(saw("PONG"), "no PONG");
|
||||
clr();
|
||||
CHECK(sub(r, "@7 PING") == RUBO_OK, "tagged PING failed");
|
||||
CHECK(saw("@7 PONG"), "tag not echoed");
|
||||
|
||||
CASE("ack then done");
|
||||
clr();
|
||||
CHECK(subrun(r, "@1 STOP") == RUBO_OK, "STOP failed");
|
||||
CHECK(saw("@1 ACK"), "no ACK");
|
||||
CHECK(saw("@1 DONE"), "no DONE");
|
||||
|
||||
CASE("unknown callable");
|
||||
clr();
|
||||
CHECK(sub(r, "WOBBLE") == RUBO_E_UNKNOWN, "expected E_UNKNOWN");
|
||||
CHECK(saw("ERR 2"), "wrong code");
|
||||
|
||||
CASE("empty sequence is legal");
|
||||
clr();
|
||||
CHECK(subrun(r, "[]") == RUBO_OK, "empty sequence rejected");
|
||||
CHECK(saw("DONE"), "no DONE for empty sequence");
|
||||
|
||||
rubo_destroy(r);
|
||||
}
|
||||
|
||||
static void test_named_args(void)
|
||||
{
|
||||
rubo_t *r = mk();
|
||||
|
||||
CASE("named args are order-independent");
|
||||
clr();
|
||||
CHECK(subrun(r, "MOVE dur=20 speed=50 dir=FWD") == RUBO_OK, "reorder failed");
|
||||
CHECK(saw("DONE"), "did not complete");
|
||||
uint32_t named_changes = rubo_hal_sim_state()->change_count;
|
||||
|
||||
rubo_hal_sim_reset();
|
||||
clr();
|
||||
CHECK(subrun(r, "MOVE FWD 50 20") == RUBO_OK, "positional failed");
|
||||
CHECK(rubo_hal_sim_state()->change_count == named_changes,
|
||||
"positional and named forms behaved differently (%u vs %u)",
|
||||
rubo_hal_sim_state()->change_count, named_changes);
|
||||
|
||||
CASE("mixed positional then named");
|
||||
clr();
|
||||
CHECK(subrun(r, "MOVE FWD speed=50 dur=20") == RUBO_OK, "mixed failed");
|
||||
|
||||
CASE("positional after named is rejected");
|
||||
clr();
|
||||
CHECK(sub(r, "MOVE dir=FWD 50 20") == RUBO_E_SYNTAX, "expected E_SYNTAX");
|
||||
CHECK(saw("positional"), "unhelpful message");
|
||||
|
||||
CASE("defaults fill in");
|
||||
clr();
|
||||
CHECK(subrun(r, "SQUARE side=20") == RUBO_OK, "default speed not applied");
|
||||
CHECK(saw("DONE"), "did not complete");
|
||||
|
||||
CASE("missing required argument");
|
||||
clr();
|
||||
CHECK(sub(r, "SQUARE") == RUBO_E_ARITY, "expected E_ARITY");
|
||||
/* Identifiers come back canonicalised to upper case (docs/grammar.md §3.4). */
|
||||
CHECK(saw("ERR 3") && saw("SIDE"), "message should name the parameter");
|
||||
|
||||
CASE("unknown parameter name");
|
||||
clr();
|
||||
CHECK(sub(r, "SQUARE side=10 velocity=3") == RUBO_E_ARGNAME, "expected E_ARGNAME");
|
||||
CHECK(saw("ERR 4"), "wrong code");
|
||||
|
||||
CASE("duplicate argument");
|
||||
clr();
|
||||
CHECK(sub(r, "SQUARE side=10 side=20") == RUBO_E_ARGNAME, "expected E_ARGNAME");
|
||||
|
||||
CASE("too many positionals");
|
||||
clr();
|
||||
CHECK(sub(r, "STOP 1") == RUBO_E_ARITY, "expected E_ARITY");
|
||||
|
||||
CASE("literal type mismatch is caught before execution");
|
||||
clr();
|
||||
CHECK(sub(r, "MOVE 5 50 20") == RUBO_E_ARGTYPE, "expected E_ARGTYPE");
|
||||
CHECK(saw("ERR 5"), "wrong code");
|
||||
|
||||
CASE("a bad word value is a range error at runtime");
|
||||
clr();
|
||||
CHECK(subrun(r, "MOVE dir=SIDEWAYS speed=10 dur=10") == RUBO_OK,
|
||||
"should parse; the word is only wrong at run time");
|
||||
CHECK(saw("ERR 6"), "expected E_RANGE");
|
||||
|
||||
rubo_destroy(r);
|
||||
}
|
||||
|
||||
static void test_definitions(void)
|
||||
{
|
||||
rubo_t *r = mk();
|
||||
|
||||
CASE("define and call");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF BOX $s [ MOVE dir=FWD speed=30 dur=$s ]") == RUBO_OK,
|
||||
"DEF failed");
|
||||
CHECK(saw("ACK") && saw("DONE"), "DEF should ACK and DONE");
|
||||
clr();
|
||||
CHECK(subrun(r, "BOX s=20") == RUBO_OK, "call failed");
|
||||
CHECK(saw("DONE"), "did not complete");
|
||||
|
||||
CASE("forward references are refused (docs/grammar.md §5.1)");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF LATER [ NOTYET ]") == RUBO_E_UNDEFINED, "expected E_UNDEFINED");
|
||||
CHECK(saw("ERR 7"), "wrong code");
|
||||
|
||||
CASE("$ must name a parameter of the enclosing procedure");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF BAD $a [ MOVE dir=FWD speed=$b dur=10 ]") == RUBO_E_SYNTAX,
|
||||
"expected E_SYNTAX");
|
||||
clr();
|
||||
CHECK(sub(r, "MOVE dir=FWD speed=$a dur=10") == RUBO_E_SYNTAX,
|
||||
"no parameters at the top level");
|
||||
|
||||
CASE("duplicate parameter");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF DUP $a $a [ STOP ]") == RUBO_E_ARGNAME, "expected E_ARGNAME");
|
||||
|
||||
CASE("defaults must come last");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF ORD $a=1 $b [ STOP ]") == RUBO_E_SYNTAX, "expected E_SYNTAX");
|
||||
|
||||
CASE("natives are neither redefinable nor deletable");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF MOVE $x [ STOP ]") == RUBO_E_READONLY, "expected E_READONLY");
|
||||
clr();
|
||||
CHECK(sub(r, "DEL MOVE") == RUBO_E_READONLY, "expected E_READONLY");
|
||||
|
||||
CASE("stdlib procedures are STATIC, so also read-only");
|
||||
clr();
|
||||
CHECK(sub(r, "DEL SQUARE") == RUBO_E_READONLY, "expected E_READONLY");
|
||||
|
||||
CASE("redefinition with the same signature is fine");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF BOX $s [ MOVE dir=BACK speed=30 dur=$s ]") == RUBO_OK,
|
||||
"same-signature redefinition rejected");
|
||||
|
||||
CASE("cycles are refused (docs/grammar.md §5.2)");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF A [ STOP ]") == RUBO_OK, "DEF A failed");
|
||||
CHECK(sub(r, "DEF B [ A ]") == RUBO_OK, "DEF B failed");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF A [ B ]") == RUBO_E_CYCLE, "expected E_CYCLE");
|
||||
CHECK(saw("ERR 8"), "wrong code");
|
||||
|
||||
CASE("signature cannot change while a caller exists");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF A $x [ STOP ]") == RUBO_E_ARITY, "expected E_ARITY");
|
||||
CHECK(saw("B"), "message should name the caller");
|
||||
|
||||
CASE("DEL refuses to dangle a caller, then succeeds once free");
|
||||
clr();
|
||||
CHECK(sub(r, "DEL A") == RUBO_E_READONLY, "A is still called by B");
|
||||
clr();
|
||||
CHECK(sub(r, "DEL B") == RUBO_OK, "DEL B failed");
|
||||
CHECK(sub(r, "DEL A") == RUBO_OK, "DEL A failed after its caller went");
|
||||
clr();
|
||||
CHECK(sub(r, "A") == RUBO_E_UNKNOWN, "A should be gone");
|
||||
|
||||
CASE("depth limit is enforced at definition time");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF P1 [ STOP ]") == RUBO_OK, "P1");
|
||||
CHECK(sub(r, "DEF P2 [ P1 ]") == RUBO_OK, "P2");
|
||||
CHECK(sub(r, "DEF P3 [ P2 ]") == RUBO_OK, "P3");
|
||||
CHECK(sub(r, "DEF P4 [ P3 ]") == RUBO_OK, "P4");
|
||||
CHECK(sub(r, "DEF P5 [ P4 ]") == RUBO_OK, "P5");
|
||||
CHECK(sub(r, "DEF P6 [ P5 ]") == RUBO_OK, "P6");
|
||||
CHECK(sub(r, "DEF P7 [ P6 ]") == RUBO_OK, "P7 should sit exactly at the limit");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF P8 [ P7 ]") == RUBO_E_LIMIT, "expected E_LIMIT");
|
||||
CHECK(saw("ERR 10"), "wrong code");
|
||||
|
||||
rubo_destroy(r);
|
||||
}
|
||||
|
||||
static void test_repeat(void)
|
||||
{
|
||||
rubo_t *r = mk();
|
||||
|
||||
CASE("REPEAT runs the body exactly n times");
|
||||
clr();
|
||||
rubo_hal_sim_reset();
|
||||
CHECK(subrun(r, "REPEAT 3 [ DRIVE left=10 right=10 dur=10 ]") == RUBO_OK,
|
||||
"REPEAT failed");
|
||||
CHECK(saw("DONE"), "did not complete");
|
||||
/* Each iteration engages (0->10) then releases (10->0). */
|
||||
CHECK(rubo_hal_sim_state()->change_count == 6,
|
||||
"expected 6 motor changes, got %u",
|
||||
rubo_hal_sim_state()->change_count);
|
||||
|
||||
CASE("REPEAT 0 skips the body");
|
||||
clr();
|
||||
rubo_hal_sim_reset();
|
||||
CHECK(subrun(r, "REPEAT 0 [ DRIVE left=10 right=10 dur=10 ]") == RUBO_OK,
|
||||
"REPEAT 0 failed");
|
||||
CHECK(rubo_hal_sim_state()->change_count == 0, "body should not have run");
|
||||
|
||||
CASE("REPEAT nests");
|
||||
clr();
|
||||
rubo_hal_sim_reset();
|
||||
CHECK(subrun(r, "REPEAT 2 [ REPEAT 3 [ DRIVE left=10 right=10 dur=5 ] ]")
|
||||
== RUBO_OK, "nested REPEAT failed");
|
||||
CHECK(rubo_hal_sim_state()->change_count == 12,
|
||||
"expected 12 motor changes, got %u",
|
||||
rubo_hal_sim_state()->change_count);
|
||||
|
||||
CASE("REPEAT takes a parameter");
|
||||
clr();
|
||||
CHECK(sub(r, "DEF NTIMES $n [ REPEAT $n [ WAIT dur=1 ] ]") == RUBO_OK,
|
||||
"DEF with REPEAT $n failed");
|
||||
clr();
|
||||
CHECK(subrun(r, "NTIMES n=3") == RUBO_OK, "call failed");
|
||||
CHECK(saw("DONE"), "did not complete");
|
||||
|
||||
CASE("a negative REPEAT count is a range error");
|
||||
clr();
|
||||
CHECK(subrun(r, "NTIMES n=-1") == RUBO_OK, "should parse");
|
||||
CHECK(saw("ERR 6"), "expected E_RANGE at run time");
|
||||
|
||||
rubo_destroy(r);
|
||||
}
|
||||
|
||||
static void test_admission_and_abort(void)
|
||||
{
|
||||
rubo_t *r = mk();
|
||||
|
||||
CASE("busy: no queue (docs/flow.md §9)");
|
||||
clr();
|
||||
CHECK(sub(r, "@1 MOVE dir=FWD speed=50 dur=300") == RUBO_OK, "MOVE failed");
|
||||
run(r, 20);
|
||||
CHECK(sub(r, "@2 STOP") == RUBO_E_BUSY, "second statement should be refused");
|
||||
CHECK(saw("@2 ERR 12"), "wrong code");
|
||||
|
||||
CASE("definitions are refused while running, too");
|
||||
clr();
|
||||
CHECK(sub(r, "@3 DEF X [ STOP ]") == RUBO_E_BUSY, "DEF should be refused");
|
||||
|
||||
CASE("control words are never refused");
|
||||
clr();
|
||||
CHECK(sub(r, "PING") == RUBO_OK, "PING refused while busy");
|
||||
CHECK(saw("PONG"), "no PONG");
|
||||
|
||||
CASE("abort has exactly one victim");
|
||||
clr();
|
||||
CHECK(sub(r, "ABORT") == RUBO_OK, "ABORT failed");
|
||||
CHECK(saw("EVT ABORT"), "no EVT ABORT");
|
||||
CHECK(saw("@1 ERR 13"), "the running program should report aborted");
|
||||
CHECK(rubo_hal_sim_state()->left == 0 && rubo_hal_sim_state()->right == 0,
|
||||
"motors still engaged after ABORT");
|
||||
CHECK(rubo_state(r) == RUBO_IDLE, "should be idle after ABORT");
|
||||
|
||||
CASE("the robot accepts work again immediately");
|
||||
clr();
|
||||
CHECK(subrun(r, "@4 STOP") == RUBO_OK, "should accept after abort");
|
||||
CHECK(saw("@4 DONE"), "no DONE");
|
||||
|
||||
CASE("motor cap");
|
||||
clr();
|
||||
CHECK(subrun(r, "MOVE dir=FWD speed=50 dur=20000") == RUBO_OK, "should parse");
|
||||
CHECK(saw("ERR 6"), "expected E_RANGE past RUBO_MOTOR_MAX_MS");
|
||||
|
||||
rubo_destroy(r);
|
||||
}
|
||||
|
||||
static void test_deadman(void)
|
||||
{
|
||||
rubo_t *r = mk();
|
||||
|
||||
CASE("a dropped link stops the robot (docs/flow.md §10)");
|
||||
clr();
|
||||
/* dur=0 means "until countermanded": the motors stay on. */
|
||||
CHECK(subrun(r, "MOVE dir=FWD speed=50 dur=0") == RUBO_OK, "MOVE failed");
|
||||
CHECK(rubo_hal_sim_state()->left != 0, "motors should be engaged");
|
||||
|
||||
clr();
|
||||
run_quiet(r, RUBO_LINK_TIMEOUT_MS + 200);
|
||||
CHECK(saw("EVT LINKLOST"), "deadman did not fire");
|
||||
CHECK(rubo_hal_sim_state()->left == 0, "motors not stopped by deadman");
|
||||
|
||||
CASE("the deadman is not armed when the motors are idle");
|
||||
clr();
|
||||
run_quiet(r, RUBO_LINK_TIMEOUT_MS + 200);
|
||||
CHECK(!saw("EVT LINKLOST"), "deadman fired on an idle robot");
|
||||
|
||||
rubo_destroy(r);
|
||||
}
|
||||
|
||||
static void test_reserved(void)
|
||||
{
|
||||
rubo_t *r = mk();
|
||||
|
||||
CASE("IF is reserved but not implemented (docs/grammar.md §7)");
|
||||
clr();
|
||||
CHECK(sub(r, "IF DIST < 30 [ STOP ]") == RUBO_E_SYNTAX, "expected E_SYNTAX");
|
||||
CHECK(saw("reserved"), "message should say it is reserved");
|
||||
|
||||
CASE("a bare ';' at the top level (docs/flow.md §4.4)");
|
||||
clr();
|
||||
CHECK(sub(r, "STOP; STOP") == RUBO_E_SYNTAX, "expected E_SYNTAX");
|
||||
CHECK(saw("brackets"), "message should suggest bracketing");
|
||||
|
||||
rubo_destroy(r);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- the framer */
|
||||
|
||||
static int feed(rubo_framer_t *f, const char *s, char *out, size_t outlen)
|
||||
{
|
||||
int emitted = 0;
|
||||
for (const char *p = s; *p; p++) {
|
||||
int st = rubo_framer_push(f, *p, 1000);
|
||||
if (st == 1) {
|
||||
emitted++;
|
||||
if (out) snprintf(out, outlen, "%s", f->buf);
|
||||
} else if (st < 0) {
|
||||
return st;
|
||||
}
|
||||
}
|
||||
return emitted;
|
||||
}
|
||||
|
||||
static void test_framer(void)
|
||||
{
|
||||
rubo_framer_t f;
|
||||
char got[RUBO_MAX_STMT + 1];
|
||||
|
||||
CASE("a statement ends at a newline only at depth zero");
|
||||
rubo_framer_reset(&f);
|
||||
CHECK(feed(&f, "STOP\n", got, sizeof got) == 1, "one-liner not emitted");
|
||||
CHECK(strcmp(got, "STOP") == 0, "got '%s'", got);
|
||||
|
||||
CASE("a multi-line DEF is one statement");
|
||||
rubo_framer_reset(&f);
|
||||
int n = feed(&f,
|
||||
"DEF SQ $s [\n"
|
||||
" REPEAT 4 [\n"
|
||||
" MOVE dir=FWD speed=50 dur=$s;\n"
|
||||
" TURN dir=RIGHT deg=90\n"
|
||||
" ]\n"
|
||||
"]\n", got, sizeof got);
|
||||
CHECK(n == 1, "expected exactly one statement, got %d", n);
|
||||
CHECK(strstr(got, "DEF SQ") && strstr(got, "TURN"),
|
||||
"reassembled wrongly: '%s'", got);
|
||||
|
||||
CASE("comments cannot desynchronise the framer");
|
||||
rubo_framer_reset(&f);
|
||||
CHECK(feed(&f, "# a [ bracket in a comment\n", got, sizeof got) == 0,
|
||||
"a comment-only line should produce no statement");
|
||||
CHECK(feed(&f, "STOP\n", got, sizeof got) == 1,
|
||||
"the framer was left out of step by a comment");
|
||||
CHECK(strcmp(got, "STOP") == 0, "got '%s'", got);
|
||||
|
||||
CASE("a trailing comment is dropped");
|
||||
rubo_framer_reset(&f);
|
||||
CHECK(feed(&f, "STOP # and then\n", got, sizeof got) == 1, "not emitted");
|
||||
CHECK(strstr(got, "#") == NULL, "comment leaked into the statement");
|
||||
|
||||
CASE("an unbalanced ']' is rejected and resynchronises");
|
||||
rubo_framer_reset(&f);
|
||||
CHECK(feed(&f, "]\n", got, sizeof got) == -(int)RUBO_E_SYNTAX,
|
||||
"expected a syntax error");
|
||||
rubo_framer_reset(&f);
|
||||
CHECK(feed(&f, "] junk\nSTOP\n", got, sizeof got) < 0, "expected an error");
|
||||
|
||||
CASE("an over-long statement is rejected");
|
||||
rubo_framer_reset(&f);
|
||||
int rc = 0;
|
||||
for (int i = 0; i < RUBO_MAX_STMT + 8 && rc >= 0; i++)
|
||||
rc = rubo_framer_push(&f, 'A', 1000);
|
||||
CHECK(rc == -(int)RUBO_E_LIMIT, "expected E_LIMIT, got %d", rc);
|
||||
|
||||
CASE("a partial statement goes stale");
|
||||
rubo_framer_reset(&f);
|
||||
for (const char *p = "DEF X ["; *p; p++) rubo_framer_push(&f, *p, 1000);
|
||||
CHECK(rubo_framer_timeout(&f, 1000) == 0, "should not be stale yet");
|
||||
CHECK(rubo_framer_timeout(&f, 1000 + RUBO_FRAME_TIMEOUT_MS + 1) < 0,
|
||||
"should have gone stale");
|
||||
|
||||
CASE("chunked arrival reassembles identically");
|
||||
rubo_framer_reset(&f);
|
||||
const char *msg = "[ STOP; WAIT dur=1 ]\n";
|
||||
int emitted = 0;
|
||||
for (const char *p = msg; *p; p++)
|
||||
if (rubo_framer_push(&f, *p, 1000) == 1) {
|
||||
emitted++;
|
||||
snprintf(got, sizeof got, "%s", f.buf);
|
||||
}
|
||||
CHECK(emitted == 1, "expected one statement");
|
||||
CHECK(strcmp(got, "[ STOP; WAIT dur=1 ]") == 0, "got '%s'", got);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- end-to-end sanity */
|
||||
|
||||
static void test_end_to_end(void)
|
||||
{
|
||||
rubo_t *r = mk();
|
||||
rubo_framer_t f;
|
||||
rubo_framer_reset(&f);
|
||||
|
||||
CASE("wire to motors");
|
||||
clr();
|
||||
const char *wire =
|
||||
"@1 DEF ZIGZAG $deg=45 $reps=2 [\n"
|
||||
" REPEAT $reps [\n"
|
||||
" TURN dir=LEFT deg=$deg;\n"
|
||||
" TURN dir=RIGHT deg=$deg\n"
|
||||
" ]\n"
|
||||
"]\n"
|
||||
"@2 ZIGZAG deg=10 reps=2\n";
|
||||
|
||||
for (const char *p = wire; *p; p++) {
|
||||
if (rubo_framer_push(&f, *p, g_now) == 1) {
|
||||
rubo_submit(r, f.buf, g_now);
|
||||
run(r, 300);
|
||||
}
|
||||
}
|
||||
if (!saw("@1 DONE") || !saw("@2 DONE")) dump();
|
||||
CHECK(saw("@1 ACK") && saw("@1 DONE"), "the definition did not complete");
|
||||
CHECK(saw("@2 ACK") && saw("@2 DONE"), "the call did not complete");
|
||||
CHECK(!saw("ERR"), "unexpected error");
|
||||
CHECK(rubo_hal_sim_state()->left == 0, "motors left running");
|
||||
|
||||
rubo_destroy(r);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_basics();
|
||||
test_named_args();
|
||||
test_definitions();
|
||||
test_repeat();
|
||||
test_admission_and_abort();
|
||||
test_deadman();
|
||||
test_reserved();
|
||||
test_framer();
|
||||
test_end_to_end();
|
||||
|
||||
printf("%d checks, %d failures\n", g_run, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user