feat(v4.0.0): the POST runner -- the kernel feeds a case and judges it from outside
v4/system/post.c: for each case it empties the node's data stack, sends DECIMAL FORTH DEFINITIONS, sends the case's lines keeping what the node prints, reads the stack, and judges: an error exactly if one is expected, and otherwise the stack and every character printed. A failing case is named with what it printed and left. Nothing of it is on the node. Its tests are in test_host_quit.c, on that test's node: 29 checks of cases that must pass and cases that must fail -- a wrong value, depth, order or output, an error wanted or unwanted, a case of two lines, depth-only cases, the starting state, more printing than is kept, and a line that never comes back. Both widths and the sanitizers. The boot still runs the capsule; that is the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c2f4b9808a
commit
2bf958ce3a
+3
-1
@@ -72,7 +72,9 @@ $(BINDIR)/san-v3blocks.o: $(V3_BLOCK_SRCS) $(wildcard $(ROOT_DIR)/v3/include/*.h
|
||||
$(LD) -r $(BINDIR)/san-v3o/*.o -o $@
|
||||
# blocks.c is compiled with each test that uses it, under that test's own
|
||||
# flags: its warnings, its sanitizers, its cell width and node size.
|
||||
blocks_src = $(if $(call uses_blocks,$(1)),$(BLOCKS_SRC))
|
||||
# So are POST's runner and its cases (system/post.c, system/post_cases.c;
|
||||
# docs/v4.0.0/NUCLEUS.md 6.3), which are the kernel's too.
|
||||
blocks_src = $(if $(call uses_blocks,$(1)),$(BLOCKS_SRC) $(HERE)/system/post.c $(HERE)/system/post_cases.c)
|
||||
blocks_objs = $(if $(call uses_blocks,$(1)),$(BINDIR)/v3blocks.o)
|
||||
san_blocks_objs = $(if $(call uses_blocks,$(1)),$(BINDIR)/san-v3blocks.o)
|
||||
|
||||
|
||||
@@ -29,4 +29,33 @@ typedef struct {
|
||||
extern const v4_post_case v4_post_cases[];
|
||||
extern const unsigned v4_post_case_count;
|
||||
|
||||
/* THE RUNNER (v4/system/post.c). Whoever runs POST on a node supplies how a
|
||||
* line is sent to it and where the console is. For each case the runner
|
||||
* empties the node's data stack from outside; sends DECIMAL FORTH
|
||||
* DEFINITIONS, the state every expected result was taken from; sends the
|
||||
* case's lines one at a time, keeping what the node prints and showing none
|
||||
* of it; and judges. A case passes when it ended in an error exactly if
|
||||
* one was expected and, if none was, its stack and its printing are what
|
||||
* the table has. It has ended in an error if any of its lines did. What
|
||||
* the cases define stays in the node's dictionary, as in v3. */
|
||||
#define V4_POST_OUT 1024u /* the most of a case's printing that is kept */
|
||||
|
||||
typedef struct {
|
||||
void *self;
|
||||
v4_node *n; /* the node: its data stack is read and emptied */
|
||||
/* Send the node a line and run it to its end. What it prints is put at
|
||||
* `out`, at most `cap` characters, and *len is how many it printed in
|
||||
* all. Returns V4_TEXT_QUIT, V4_TEXT_COMPLETED or V4_TEXT_ERROR
|
||||
* (message.h); or a negative number if the line did not come back. */
|
||||
int (*line)(void *self, const char *text, unsigned text_len, char *out, unsigned cap, unsigned *len);
|
||||
void (*say)(void *self, const char *text, unsigned len); /* the console */
|
||||
} v4_post_host;
|
||||
|
||||
typedef struct { unsigned tests, pass, fail; } v4_post_tally;
|
||||
|
||||
/* Run the cases. Prints "POST FAIL: name out<...> stack<...>" for each
|
||||
* failing case and then "PARITY:V4_POST tests=N pass=N fail=N". Returns 1
|
||||
* if none failed. */
|
||||
int v4_post_run(const v4_post_host *h, const v4_post_case *cases, unsigned count, v4_post_tally *tally);
|
||||
|
||||
#endif /* V4_POST_H */
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/* post.c -- POST's runner: the kernel feeds a case to the node and judges it
|
||||
* from outside. See post.h; docs/v4.0.0/NUCLEUS.md section 6.3.
|
||||
*
|
||||
* Nothing here uses the C library: the bare-metal kernel links this file.
|
||||
*/
|
||||
#include "v4/post.h"
|
||||
#include "v4/message.h"
|
||||
|
||||
#define SHOWN_OUT 255u /* how much of a failing case's printing is shown */
|
||||
#define SHOWN_STACK 32u /* and how many of the values it left */
|
||||
|
||||
static const char state_line[] = "DECIMAL FORTH DEFINITIONS";
|
||||
|
||||
static unsigned length(const char *s)
|
||||
{
|
||||
unsigned n = 0;
|
||||
while (s[n]) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static void say(const v4_post_host *h, const char *s)
|
||||
{
|
||||
h->say(h->self, s, length(s));
|
||||
}
|
||||
|
||||
static void say_number(const v4_post_host *h, long long v)
|
||||
{
|
||||
char text[24];
|
||||
unsigned at = sizeof text;
|
||||
unsigned long long u = v < 0 ? 0ull - (unsigned long long)v : (unsigned long long)v;
|
||||
|
||||
do { text[--at] = (char)('0' + u % 10u); u /= 10u; } while (u);
|
||||
if (v < 0) text[--at] = '-';
|
||||
h->say(h->self, text + at, (unsigned)sizeof text - at);
|
||||
}
|
||||
|
||||
int v4_post_run(const v4_post_host *h, const v4_post_case *cases, unsigned count, v4_post_tally *tally)
|
||||
{
|
||||
static char out[V4_POST_OUT]; /* what the case being run has printed */
|
||||
v4_cell left[SHOWN_STACK]; /* what it left on the stack, the top first */
|
||||
unsigned i, k;
|
||||
|
||||
tally->tests = 0; tally->pass = 0; tally->fail = 0;
|
||||
for (i = 0; i < count; i++) {
|
||||
const v4_post_case *c = &cases[i];
|
||||
unsigned kept = 0, total = 0, len = 0, depth;
|
||||
int errored = 0, came_back = 1, ok, how;
|
||||
|
||||
/* every case starts from the same state */
|
||||
v4_dstack_reset(&h->n->ds);
|
||||
how = h->line(h->self, state_line, (unsigned)sizeof state_line - 1u, out, 0u, &len);
|
||||
if (how != V4_TEXT_COMPLETED) came_back = 0;
|
||||
v4_dstack_reset(&h->n->ds);
|
||||
|
||||
for (k = 0; came_back && k < c->line_count; k++) {
|
||||
len = 0;
|
||||
how = h->line(h->self, c->lines[k], length(c->lines[k]), out + kept, V4_POST_OUT - kept, &len);
|
||||
if (how < 0) { came_back = 0; break; }
|
||||
if (how == V4_TEXT_ERROR) errored = 1;
|
||||
total += len;
|
||||
kept = total < V4_POST_OUT ? total : V4_POST_OUT;
|
||||
}
|
||||
|
||||
/* what it left, read before anything is judged */
|
||||
depth = h->n->ds.depth;
|
||||
for (k = 0; k < depth; k++) {
|
||||
v4_cell v = v4_dstack_pop(&h->n->ds);
|
||||
if (k < SHOWN_STACK) left[k] = v;
|
||||
}
|
||||
v4_dstack_reset(&h->n->ds);
|
||||
|
||||
if (!came_back) {
|
||||
ok = 0;
|
||||
} else if (c->expect == V4_POST_ERROR) {
|
||||
ok = errored;
|
||||
} else if (errored) {
|
||||
ok = 0;
|
||||
} else {
|
||||
ok = depth == c->depth;
|
||||
if (ok && c->expect == V4_POST_STACK) {
|
||||
if (depth > SHOWN_STACK) ok = 0;
|
||||
for (k = 0; ok && k < depth; k++) ok = left[k] == (v4_cell)c->stack[depth - 1u - k];
|
||||
}
|
||||
if (ok && c->expect != V4_POST_DEPTH) {
|
||||
ok = total == c->output_len && total <= V4_POST_OUT;
|
||||
for (k = 0; ok && k < total; k++) ok = out[k] == c->output[k];
|
||||
}
|
||||
}
|
||||
|
||||
tally->tests++;
|
||||
if (ok) { tally->pass++; continue; }
|
||||
tally->fail++;
|
||||
say(h, "POST FAIL: "); say(h, c->name);
|
||||
say(h, " out<"); h->say(h->self, out, kept < SHOWN_OUT ? kept : SHOWN_OUT);
|
||||
say(h, "> stack<");
|
||||
for (k = depth < SHOWN_STACK ? depth : SHOWN_STACK; k-- > 0; ) { say_number(h, (long long)left[k]); say(h, " "); }
|
||||
say(h, ">\n");
|
||||
}
|
||||
say(h, "PARITY:V4_POST tests="); say_number(h, (long long)tally->tests);
|
||||
say(h, " pass="); say_number(h, (long long)tally->pass);
|
||||
say(h, " fail="); say_number(h, (long long)tally->fail);
|
||||
say(h, "\n");
|
||||
return tally->fail == 0;
|
||||
}
|
||||
@@ -53,6 +53,7 @@ static int failures = 0, checks = 0;
|
||||
#define CANARY ((v4_cell)0x0C0FFEE5)
|
||||
|
||||
#include "v4/blocks.h"
|
||||
#include "v4/post.h"
|
||||
#include "block_subsystem.h"
|
||||
#include "host_map.h"
|
||||
|
||||
@@ -358,6 +359,53 @@ static const char *say(const char *input)
|
||||
}
|
||||
return out;
|
||||
}
|
||||
/* POST's RUNNER (v4/system/post.c), with these tests as its host: a line is
|
||||
* sent to the node as the console sends one, and what the node prints is
|
||||
* handed back and not shown. `post_lost` is the text of a line that is
|
||||
* never to come back, and `post_state_fails` makes the line that sets the
|
||||
* state end in an error; `post_said` is what the runner printed. */
|
||||
static const char *post_lost;
|
||||
static int post_state_fails;
|
||||
static char post_said[4096];
|
||||
static unsigned post_said_len;
|
||||
static int post_line(void *self, const char *text, unsigned text_len, char *o, unsigned cap, unsigned *len)
|
||||
{
|
||||
int ended;
|
||||
(void)self;
|
||||
*len = 0;
|
||||
if (post_lost && strlen(post_lost) == text_len && memcmp(post_lost, text, text_len) == 0) return -1;
|
||||
if (post_state_fails && text_len == 25 && memcmp(text, "DECIMAL FORTH DEFINITIONS", 25) == 0) text = "NOSUCHWORD-FOR-STATE ";
|
||||
if (!v4_message_text(&to_node, 0, 1, V4_MSG_TEXT, text, text_len)) return -1;
|
||||
to_sent = 0; from_node.count = 0;
|
||||
shown_len = 0; shown_dropped = 0;
|
||||
ended = run_line(40000000);
|
||||
if (ended != 1) return -1;
|
||||
*len = shown_dropped ? (unsigned)sizeof shown + 1u : shown_len;
|
||||
memcpy(o, shown, shown_len < cap ? shown_len : cap);
|
||||
return text_ended;
|
||||
}
|
||||
static void post_say(void *self, const char *text, unsigned len)
|
||||
{
|
||||
(void)self;
|
||||
if (post_said_len + len < sizeof post_said) { memcpy(post_said + post_said_len, text, len); post_said_len += len; post_said[post_said_len] = 0; }
|
||||
}
|
||||
static const v4_post_host post_host = { 0, &n, post_line, post_say };
|
||||
/* Run one case; 1 if it passed. What the runner said is in post_said. */
|
||||
static int post_one(const char *name, const char *l0, const char *l1, int expect, const long long *stack, unsigned depth, const char *output)
|
||||
{
|
||||
static const char *lines[2];
|
||||
v4_post_case c;
|
||||
v4_post_tally t;
|
||||
int ok;
|
||||
lines[0] = l0; lines[1] = l1;
|
||||
c.name = name; c.lines = lines; c.line_count = l1 ? 2u : 1u; c.expect = expect;
|
||||
c.stack = stack; c.depth = depth; c.output = output; c.output_len = (unsigned)strlen(output);
|
||||
post_said_len = 0; post_said[0] = 0;
|
||||
ok = v4_post_run(&post_host, &c, 1, &t);
|
||||
if (t.tests != 1 || t.pass + t.fail != 1 || (int)t.pass != ok) return -1;
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Feed a file of FORTH source to the prompt, a line at a time, as if typed.
|
||||
* True if every line was accepted: the node said " ok" and nothing else. */
|
||||
static int load_source(const char *name)
|
||||
@@ -1172,6 +1220,63 @@ int main(void)
|
||||
CHECK(is(say("FORGET G3 G1 G2\n"), "AB ok\nok> ") && is(say("G3\n"), "UNKNOWN WORD: 'G3'\n ERROR\nok> "), "one above it can");
|
||||
}
|
||||
|
||||
/* ---- POST's runner: the kernel feeds a case and judges it from outside (NUCLEUS.md 6.3) ---- */
|
||||
{
|
||||
static const long long three[] = { 3 }, four[] = { 4 }, two_vals[] = { 3, 3 }, seven[] = { 7 }, ab[] = { 1, 2 };
|
||||
static char big[2100];
|
||||
boot();
|
||||
CHECK(post_one("add.right", "1 2 +", 0, V4_POST_STACK, three, 1, "") == 1 && strstr(post_said, "POST FAIL") == NULL
|
||||
&& strstr(post_said, "PARITY:V4_POST tests=1 pass=1 fail=0\n") != NULL, "a case that leaves what the table has passes, and the tally says so: \"%s\"", post_said);
|
||||
CHECK(post_one("add.wrong", "1 2 +", 0, V4_POST_STACK, four, 1, "") == 0 && strstr(post_said, "POST FAIL: add.wrong ") != NULL && strstr(post_said, "stack<3 >") != NULL
|
||||
&& strstr(post_said, "PARITY:V4_POST tests=1 pass=0 fail=1\n") != NULL, "one that leaves another value fails, by name, with the stack it left: \"%s\"", post_said);
|
||||
CHECK(post_one("add.depth", "1 2 +", 0, V4_POST_STACK, two_vals, 2, "") == 0, "one that leaves fewer values than the table has fails");
|
||||
CHECK(post_one("two.order", "1 2", 0, V4_POST_STACK, ab, 2, "") == 1 && post_one("two.swapped", "2 1", 0, V4_POST_STACK, ab, 2, "") == 0, "the stack is compared in order, the deepest first");
|
||||
CHECK(post_one("dot.right", "5 DUP . . CR", 0, V4_POST_STACK, 0, 0, "5 5 \n") == 1, "what a case prints is compared");
|
||||
CHECK(strstr(post_said, "5 5") == NULL, "and what a passing case prints does not reach the console: \"%s\"", post_said);
|
||||
CHECK(post_one("emit.wrong", "65 EMIT", 0, V4_POST_STACK, 0, 0, "B") == 0 && strstr(post_said, "POST FAIL: emit.wrong out<A>") != NULL, "one that prints another character fails, with what it printed: \"%s\"", post_said);
|
||||
CHECK(post_one("emit.short", "65 EMIT 66 EMIT", 0, V4_POST_STACK, 0, 0, "A") == 0 && post_one("emit.long", "65 EMIT", 0, V4_POST_STACK, 0, 0, "AB") == 0, "and one that prints more, or less");
|
||||
CHECK(post_one("err.right", "NOSUCHWORD", 0, V4_POST_ERROR, 0, 0, "") == 1, "a case that must end in an error and does passes");
|
||||
CHECK(strstr(post_said, "UNKNOWN") == NULL, "and the message of an expected error does not reach the console");
|
||||
CHECK(post_one("err.none", "1 2 +", 0, V4_POST_ERROR, 0, 0, "") == 0, "one that must end in an error and does not fails");
|
||||
CHECK(post_one("err.unwanted", "NOSUCHWORD", 0, V4_POST_STACK, 0, 0, "") == 0, "one that ends in an error and must not fails");
|
||||
CHECK(post_one("err.first", "NOSUCHWORD", "1 DROP", V4_POST_ERROR, 0, 0, "") == 1, "a case has ended in an error if any of its lines did");
|
||||
CHECK(post_one("two.lines", ": PQ 7 ;", "PQ", V4_POST_STACK, seven, 1, "") == 1, "a case of two lines is judged on what both leave");
|
||||
CHECK(is(say("PQ .\n"), "7 ok\nok> "), "and what a case defines stays on the node");
|
||||
CHECK(post_one("here.depth", "HERE", 0, V4_POST_DEPTH, 0, 1, "") == 1 && post_one("here.depth2", "HERE", 0, V4_POST_DEPTH, 0, 2, "") == 0, "a case may be checked for how many values it leaves and no more");
|
||||
CHECK(post_one("here.out", "HERE 65 EMIT", 0, V4_POST_DEPTH_OUTPUT, 0, 1, "A") == 1 && post_one("here.out2", "HERE 65 EMIT", 0, V4_POST_DEPTH_OUTPUT, 0, 1, "B") == 0, "or for that and what it prints");
|
||||
CHECK(post_one("depth.noout", "HERE 65 EMIT", 0, V4_POST_DEPTH, 0, 1, "") == 1, "a case checked for depth alone may print what it likes");
|
||||
|
||||
/* the state every case starts from */
|
||||
CHECK(is(say("1 2 3 HEX\n"), " ok\nok> "), "the node is left in HEX with three values on its stack");
|
||||
CHECK(post_one("state", "10 .", 0, V4_POST_STACK, 0, 0, "10 ") == 1, "a case still starts in decimal with nothing on the stack");
|
||||
post_state_fails = 1;
|
||||
CHECK(post_one("state.bad", "1 2 +", 0, V4_POST_STACK, three, 1, "") == 0 && strstr(post_said, "POST FAIL: state.bad ") != NULL, "if the state cannot be set the case fails, by name");
|
||||
post_state_fails = 0;
|
||||
|
||||
/* a case that prints more than is kept */
|
||||
CHECK(is(say(": MANY 2000 0 DO 65 EMIT LOOP ;\n"), " ok\nok> "), "a word that prints 2,000 characters");
|
||||
memset(big, 'A', 2000); big[2000] = 0;
|
||||
CHECK(post_one("many", "MANY", 0, V4_POST_STACK, 0, 0, big) == 0 && strstr(post_said, "POST FAIL: many ") != NULL, "a case that prints more than the runner keeps fails, by name");
|
||||
CHECK(post_one("after.many", "1 2 +", 0, V4_POST_STACK, three, 1, "") == 1, "and the next case is judged as usual");
|
||||
|
||||
/* a line that never comes back */
|
||||
post_lost = "LOST-LINE";
|
||||
{
|
||||
static const char *const la[] = { "1 2 +" }, *const lb[] = { "LOST-LINE" }, *const lc[] = { "65 EMIT" };
|
||||
static const v4_post_case three_cases[] = {
|
||||
{ "first", la, 1u, V4_POST_STACK, three, 1u, "", 0u },
|
||||
{ "lost", lb, 1u, V4_POST_STACK, 0, 0u, "", 0u },
|
||||
{ "last", lc, 1u, V4_POST_STACK, 0, 0u, "A", 1u },
|
||||
};
|
||||
v4_post_tally t;
|
||||
post_said_len = 0; post_said[0] = 0;
|
||||
CHECK(v4_post_run(&post_host, three_cases, 3, &t) == 0 && t.tests == 3 && t.pass == 2 && t.fail == 1
|
||||
&& strstr(post_said, "POST FAIL: lost ") != NULL && strstr(post_said, "PARITY:V4_POST tests=3 pass=2 fail=1\n") != NULL,
|
||||
"a line that does not come back fails its case; the others are still judged, and the run is not a pass: \"%s\"", post_said);
|
||||
}
|
||||
post_lost = 0;
|
||||
}
|
||||
|
||||
/* ---- storage that refuses a write (MESH.md 8.2) ---- */
|
||||
boot();
|
||||
CHECK(is(say(REFUSING_FIRST " BLOCK DROP UPDATE SAVE-BUFFERS 65 EMIT\n"), "Storage refused\n ERROR\nok> "), "a write storage will not take says so, and the prompt returns");
|
||||
|
||||
Reference in New Issue
Block a user