aboutsummaryrefslogtreecommitdiffstats
path: root/core/test-module.c
diff options
context:
space:
mode:
Diffstat (limited to 'core/test-module.c')
-rw-r--r--core/test-module.c244
1 files changed, 243 insertions, 1 deletions
diff --git a/core/test-module.c b/core/test-module.c
index 6c76510..6117fe4 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -632,7 +632,7 @@ static void messages(void) {
static const H note_handler[] = {{1, 0, 2}};
bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "document operation outside"));
- static const F unknown_type[] = {{"insert", 5}, {"start", 1}};
+ static const F unknown_type[] = {{"insert", 6}, {"start", 1}};
static const I misplaced[] = {{"edit", NULL, PURE_KIND_MESSAGE, true, unknown_type, 2}};
bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "message field"));
@@ -2427,6 +2427,246 @@ static void lane_folds(void) {
"\"cells\":%u}\n", folds, planned, shaped[PURE_SHAPE_GROUP], shaped[PURE_SHAPE_RANK], passes, faults, cells);
}
+/* ---- Walks and a message's rows ----------------------------------------
+ * A shop: stock is keyed by item; an order's rows (a message's rows field,
+ * filling the list order.lines) each take their quantity from the item's
+ * stock and are logged, all or nothing (an item not stocked, or not enough of
+ * it, fails the order); clean walks the stock, removing an item with none
+ * left, removing the first item for one with one left, and adding one to
+ * the others. Random events against a model: the walk visits every item
+ * still there when it comes, once, in order, and the rows are gone after
+ * every event. */
+enum { EACH = PURE_OP_EACH, W_N = 0, W_SUM = 1, W_STOCK = 2, W_LINES = 3, W_LOG = 4 };
+#define WALKING(list, length) ((uint32_t)(list) << 8 | (uint32_t)(length) << 16)
+#define ITEM(list, field) ((uint32_t)(list) | (uint32_t)(field) << 16)
+static const S walk_numbers[] = {{"n", 0}, {"sum", 0}};
+static const T walk_lists[] = {{"stock", 3 | 1 << 8, 0}, {"order.lines", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}, {"log", 3, 2}};
+static const RF stock_fields[] = {{"item", 0, 0}, {"left", 0, 0}}, line_fields[] = {{"item", 0, 0}, {"qty", 0, 0}},
+ entry_fields[] = {{"item", 0, 0}, {"qty", 0, 0}};
+static const R walk_records[] = {{"Stock", stock_fields, 2}, {"Line", line_fields, 2}, {"Entry", entry_fields, 2}};
+static const F order_fields[] = {{"lines", PURE_FIELD_ROWS | W_LINES << 8}}, restock_fields[] = {{"item", 1}, {"left", 1}};
+static const I walk_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, order_fields, 1}, KEY("clean", "KeyC"),
+ {"restock", NULL, PURE_KIND_MESSAGE, false, restock_fields, 2}};
+static const O walk_code[] = {
+ /* 0 order: for l in order.lines { stock[l.item].left -= l.qty (not below 0); log.append(l) } */
+ {EACH, WALKING(W_LINES, 20), 0},
+ {ROW, W_LINES, 0}, {IG, ITEM(W_LINES, 0), 0}, {PURE_OP_LIST_FIND, W_STOCK, 0},
+ {ROW, W_LINES, 0}, {IG, ITEM(W_LINES, 0), 0}, {PURE_OP_LIST_FIND, W_STOCK, 0}, {IG, ITEM(W_STOCK, 1), 0},
+ {ROW, W_LINES, 0}, {IG, ITEM(W_LINES, 1), 0}, {PURE_OP_SUB, 0, 0},
+ {PICK, 0, 0}, {C, 0, 0}, {PURE_OP_GE, 0, 0}, {REQ, 0, 0},
+ {IS, ITEM(W_STOCK, 1), 0},
+ {ROW, W_LINES, 0}, {IG, ITEM(W_LINES, 0), 0}, {ROW, W_LINES, 0}, {IG, ITEM(W_LINES, 1), 0}, {LA, W_LOG, 0},
+ /* 21: n += 1; sum += sum(l.qty for l in order.lines) */
+ {L, W_N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, W_N, 0},
+ {L, W_SUM, 0}, {FOLD, FOLDING(PURE_FOLD_SUM, W_LINES, 3), 0}, {ROW, W_LINES, 0}, {IG, ITEM(W_LINES, 1), 0},
+ {C, 0, 1}, {ADD, 0, 0}, {ST, W_SUM, 0},
+ /* 32 clean: for s in stock { if s.left == 0 { stock.remove(s) } else if s.left == 1 { stock.remove(stock.first) }
+ * else { s.left = s.left + 1 } } */
+ {EACH, WALKING(W_STOCK, 22), 0},
+ {ROW, W_STOCK, 0}, {IG, ITEM(W_STOCK, 1), 0}, {C, 0, 0}, {EQ, 0, 0}, {JZ, 41, 0},
+ {ROW, W_STOCK, 0}, {LR, W_STOCK, 0}, {JMP, 55, 0},
+ {ROW, W_STOCK, 0}, {IG, ITEM(W_STOCK, 1), 0}, {C, 0, 1}, {EQ, 0, 0}, {JZ, 49, 0},
+ {XF, W_STOCK, 0}, {LR, W_STOCK, 0}, {JMP, 55, 0},
+ {ROW, W_STOCK, 0}, {ROW, W_STOCK, 0}, {IG, ITEM(W_STOCK, 1), 0}, {C, 0, 1}, {ADD, 0, 0}, {IS, ITEM(W_STOCK, 1), 0},
+ /* 55 restock: stock.add(Stock { item, left }) */
+ {FIELD, 0, 0}, {FIELD, 1, 0}, {LA, W_STOCK, 0},
+};
+static const H walk_handlers[] = {{1, 0, 32}, {3, 32, 23}, {5, 55, 3}};
+
+static Spec walk_spec(const O *code, size_t count, const I *inputs, size_t ninputs, const H *handlers, size_t nhandlers,
+ const T *lists, const V *views, size_t nviews) {
+ return (Spec){walk_numbers, LEN(walk_numbers), inputs, ninputs, views, nviews, handlers, nhandlers, code, count, NULL, 0,
+ lists, 3, NULL, 0, NULL, 0, walk_records, LEN(walk_records), NULL, 0};
+}
+
+typedef struct { double item, left; } StockItem;
+typedef struct { StockItem stock[64]; size_t stocked; double log[4096][2]; size_t logged; double n, sum; } Shop;
+
+static bool same_shop(const PureApp *app, const Shop *shop) {
+ const PureList *stock = app->lists[W_STOCK], *log = app->lists[W_LOG];
+ if (pure_list_count(stock) != shop->stocked || pure_list_count(log) != shop->logged ||
+ pure_list_count(app->lists[W_LINES]) || app->values[W_N] != shop->n || app->values[W_SUM] != shop->sum)
+ return false;
+ for (size_t i = 0; i < shop->stocked; ++i)
+ if (pure_list_get(stock, i, 0).num != shop->stock[i].item || pure_list_get(stock, i, 1).num != shop->stock[i].left)
+ return false;
+ for (size_t i = 0; i < shop->logged; ++i)
+ if (pure_list_get(log, i, 0).num != shop->log[i][0] || pure_list_get(log, i, 1).num != shop->log[i][1]) return false;
+ return true;
+}
+
+static size_t put_row(unsigned char *out, double item, double qty) {
+ put_double(out, item);
+ put_double(out + 8, qty);
+ return 16;
+}
+
+static void walks(void) {
+ CHECK(load(walk_spec(walk_code, LEN(walk_code), walk_inputs, 3, walk_handlers, 3, walk_lists, NULL, 0), &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ static Shop shop, before;
+ shop = (Shop){.stocked = 0};
+ uint32_t seed = 0x5a1e5eedu;
+ unsigned orders = 0, failed = 0, cleaned = 0, removed = 0, earlier = 0;
+ for (unsigned round = 0; round < 3000; ++round) {
+ before = shop;
+ unsigned roll_ = next_random(&seed) % 10;
+ PureResult result;
+ bool ok = true;
+ if (roll_ < 3) { /* restock an item, maybe one there already */
+ double item = (double)(next_random(&seed) % 12), left = (double)(next_random(&seed) % 4);
+ unsigned char payload[8];
+ put32(payload, (uint32_t)item);
+ put32(payload + 4, (uint32_t)left);
+ result = pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8});
+ size_t at = 0;
+ while (at < shop.stocked && shop.stock[at].item < item) ++at;
+ if (at < shop.stocked && shop.stock[at].item == item) ok = false;
+ else {
+ memmove(&shop.stock[at + 1], &shop.stock[at], (shop.stocked - at) * sizeof(*shop.stock));
+ shop.stock[at] = (StockItem){item, left};
+ ++shop.stocked;
+ }
+ } else if (roll_ < 8) { /* an order of up to 6 lines */
+ unsigned char payload[6 * 16];
+ size_t size = 0, lines = next_random(&seed) % 7;
+ double qty_total = 0;
+ for (size_t k = 0; k < lines && ok; ++k) {
+ double item = (double)(next_random(&seed) % 13), qty = (double)(next_random(&seed) % 3);
+ size += put_row(payload + size, item, qty);
+ size_t at = 0;
+ while (at < shop.stocked && shop.stock[at].item != item) ++at;
+ if (at == shop.stocked || shop.stock[at].left - qty < 0) { ok = false; break; }
+ shop.stock[at].left -= qty;
+ shop.log[shop.logged][0] = item;
+ shop.log[shop.logged++][1] = qty;
+ qty_total += qty;
+ }
+ for (size_t k = 0; !ok && k < lines; ++k) /* the rest of the rows, for the payload */
+ if (size < (k + 1) * 16) size += put_row(payload + size, (double)(next_random(&seed) % 13), 1);
+ result = pure_app_message(&app, 0, NULL, -1, (PureSpan){size ? payload : NULL, size});
+ if (ok) {
+ shop.n += 1;
+ shop.sum += qty_total;
+ ++orders;
+ }
+ } else { /* clean */
+ result = pure_app_input(&app, 1, PURE_EDGE_DOWN);
+ size_t at = 0;
+ while (at < shop.stocked) {
+ bool gone = false;
+ if (shop.stock[at].left == 0) {
+ memmove(&shop.stock[at], &shop.stock[at + 1], (shop.stocked - at - 1) * sizeof(*shop.stock));
+ --shop.stocked;
+ gone = true;
+ ++removed;
+ } else if (shop.stock[at].left == 1) {
+ memmove(&shop.stock[0], &shop.stock[1], (shop.stocked - 1) * sizeof(*shop.stock));
+ --shop.stocked;
+ if (at == 0) gone = true;
+ else --at, ++earlier;
+ ++removed;
+ } else
+ shop.stock[at].left += 1;
+ if (!gone) ++at;
+ }
+ ++cleaned;
+ pure_app_input(&app, 1, PURE_EDGE_UP);
+ }
+ if (!ok) {
+ shop = before;
+ ++failed;
+ }
+ if ((result == PURE_OK) != ok || !same_shop(&app, &shop)) {
+ fprintf(stderr, "walk round %u: result %d, expected %s\n", round, result, ok ? "ok" : "a fault");
+ abort();
+ }
+ if (shop.logged > 4000) { /* start over before the model's log fills */
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ shop = (Shop){.stocked = 0};
+ }
+ }
+ /* Rows that are not whole fail the message and change nothing. */
+ before = shop;
+ unsigned char short_row[12] = {0}, not_finite[16];
+ put_double(not_finite, 1);
+ put_double(not_finite + 8, NAN);
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){short_row, 12}) == PURE_LIMIT && same_shop(&app, &before));
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){not_finite, 16}) == PURE_LIMIT && same_shop(&app, &before));
+ pure_app_stop(&app);
+
+ /* What a walk and a rows field may not be. */
+ static const F note_fields[] = {{"lines", PURE_FIELD_ROWS | W_LINES << 8}, {"text", 4}};
+ static const I note_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, note_fields, 2}};
+ static const O into_walked[] = {{EACH, WALKING(W_STOCK, 3), 0}, {C, 0, 1}, {C, 0, 2}, {LA, W_STOCK, 0}};
+ static const O nested[] = {{EACH, WALKING(W_STOCK, 3), 0}, {EACH, WALKING(W_LOG, 2), 0}, {C, 0, 1}, {ST, W_N, 0}};
+ static const O replied[] = {{EACH, WALKING(W_STOCK, 2), 0}, {FIELD, 1, 0}, {REPLY, 0, 0}};
+ static const O rows_read[] = {{FIELD, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {REPLY, 0, 0}};
+ static const O into_each[] = {{C, 0, 1}, {JZ, 3, 0}, {EACH, WALKING(W_STOCK, 2), 0}, {C, 0, 1}, {ST, W_N, 0}};
+ static const O view_each[] = {{EACH, WALKING(W_STOCK, 2), 0}, {C, 0, 1}, {ST, W_N, 0}, {C, 0, 1}};
+ static const H first3[] = {{1, 0, 4}}, first3b[] = {{1, 0, 3}}, first5[] = {{1, 0, 5}};
+ static const V walk_view[] = {{"v", 0, 4}};
+ static const T unnamed[] = {{"stock", 3 | 1 << 8, 0}, {"lines", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}, {"log", 3, 2}};
+ static const F logged_rows[] = {{"lines", PURE_FIELD_ROWS | W_LOG << 8}};
+ static const I logged_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, logged_rows, 1}};
+ static const I sent_rows[] = {{"order", NULL, PURE_KIND_OUTPUT, false, order_fields, 1}};
+ const struct { Spec spec; const char *reason; } refused[] = {
+ {walk_spec(into_walked, LEN(into_walked), note_inputs, 1, first3, 1, walk_lists, NULL, 0), "insert into the list walked"},
+ {walk_spec(nested, LEN(nested), note_inputs, 1, first3, 1, walk_lists, NULL, 0), "each inside each"},
+ {walk_spec(replied, LEN(replied), note_inputs, 1, first3b, 1, walk_lists, NULL, 0), "reply inside each"},
+ {walk_spec(rows_read, LEN(rows_read), note_inputs, 1, first3b, 1, walk_lists, NULL, 0), "no such state, field"},
+ {walk_spec(into_each, LEN(into_each), note_inputs, 1, first5, 1, walk_lists, NULL, 0), "branch into an each"},
+ {walk_spec(view_each, LEN(view_each), note_inputs, 1, NULL, 0, walk_lists, walk_view, 1), "view with an effect"},
+ {walk_spec(NULL, 0, note_inputs, 1, NULL, 0, unnamed, NULL, 0), "state name"},
+ {walk_spec(NULL, 0, logged_inputs, 1, NULL, 0, walk_lists, NULL, 0), "rows field"},
+ {walk_spec(NULL, 0, sent_rows, 1, NULL, 0, walk_lists, NULL, 0), "rows field"},
+ };
+ for (size_t i = 0; i < LEN(refused); ++i)
+ if (load(refused[i].spec, &module_b) != PURE_INVALID || !strstr(error, refused[i].reason)) {
+ fprintf(stderr, "walk case %zu: expected '%s', got '%s'\n", i, refused[i].reason, error);
+ abort();
+ }
+ CHECK(orders > 300 && failed > 300 && cleaned > 200 && removed > 50 && earlier > 10);
+ printf("{\"walks\":{\"orders\":%u,\"failed\":%u,\"cleaned\":%u,\"removed_while_walking\":%u,\"removed_before\":%u}}\n",
+ orders, failed, cleaned, removed, earlier);
+}
+
+/* Damaged walk modules: whatever loads runs safely, rows and all. */
+static void walk_mutations(void) {
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, walk_spec(walk_code, LEN(walk_code), walk_inputs, 3, walk_handlers, 3, walk_lists, NULL, 0));
+ uint32_t state = 0x2f6b1c3du;
+ unsigned accepted = 0;
+ for (unsigned round = 0; round < 20000; ++round) {
+ memcpy(bytes, valid, size);
+ for (unsigned f = 0, flips = 1 + next_random(&state) % 3; f < flips; ++f)
+ bytes[next_random(&state) % size] ^= (unsigned char)(1u << (next_random(&state) % 8));
+ if (pure_module_load(bytes, size, &module_b, error, sizeof(error)) != PURE_OK) continue;
+ ++accepted;
+ PureApp app;
+ if (pure_app_start(&app, &module_b, (PureData){0}) != PURE_OK) continue;
+ unsigned char payload[96];
+ for (unsigned e = 0; e < 24; ++e) {
+ uint32_t input = next_random(&state) % module_b.inputs;
+ size_t length = next_random(&state) % sizeof(payload);
+ for (size_t k = 0; k < length; ++k) payload[k] = (unsigned char)next_random(&state);
+ for (size_t k = 0; k + 16 <= length && next_random(&state) % 2; k += 16)
+ put_row(payload + k, (double)(next_random(&state) % 8), (double)(next_random(&state) % 3));
+ if (module_b.input[input].kind == PURE_KIND_MESSAGE)
+ pure_app_message(&app, input, NULL, -1, (PureSpan){payload, length});
+ else if (module_b.input[input].kind == PURE_KIND_KEY)
+ pure_app_input(&app, input, PURE_EDGE_DOWN);
+ for (uint32_t s_ = 0; s_ < module_b.states; ++s_)
+ if (module_b.state_type[s_] == PURE_STATE_LIST && module_b.state_flags[s_] & PURE_LIST_ROWS >> 8)
+ CHECK(!pure_list_count(app.lists[s_])); /* rows go with their event */
+ }
+ pure_app_stop(&app);
+ }
+ printf("{\"walk_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
+}
+
/* Damaged fold and filter modules: whatever loads runs safely. */
static void fold_mutations(void) {
static unsigned char valid[65536], bytes[65536];
@@ -3348,6 +3588,8 @@ int main(void) {
folds();
filters();
lane_folds();
+ walks();
+ walk_mutations();
keyed();
keyed_mutations();
snapshots();