448 lines
18 KiB
C
448 lines
18 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* Unit tests for speculative decoding under the tool-call grammar
|
|
* (src/serve/grammar_spec_chooser.c) with the real ornith JSON grammar in its
|
|
* tool_choice "required" think-gated mode.
|
|
*
|
|
* The central test is an EQUIVALENCE SIMULATION: a deterministic toy model
|
|
* (logits = a hash of the token prefix, over a small vocab of real ornith
|
|
* fragments) is decoded twice —
|
|
* plain : one token at a time, grammar mask on the dense row, argmax;
|
|
* spec : seed + drafts verified in one cycle exactly as
|
|
* pn_qwen35moe_tt_spec_step_ex accepts them (a draft is accepted iff
|
|
* the chooser picks it; DEFER ends the cycle at the previous row),
|
|
* rows handed over as top-N pairs, drafts mixing the true
|
|
* continuation with grammar-breaking junk —
|
|
* and the two token streams must be identical for every trial. Every
|
|
* completed call must parse with the serve's own parser (tc_parse_tool_calls).
|
|
*
|
|
* The simulation is SELF-FALSIFYING: the same spec loop with the grammar
|
|
* ignored (mask never active) must DIVERGE from plain decoding in some trial,
|
|
* or the test exits 2 (VACUOUS) — a comparison that cannot fail proves nothing.
|
|
*
|
|
* Pure C, libc-only — no engine/model/device.
|
|
*/
|
|
|
|
#include "lib/test.h"
|
|
#include "../src/serve/grammar_spec_chooser.h"
|
|
#include "../src/core/grammar_mask_shortlist.h"
|
|
#include "../src/serve/tool_grammar_json.h"
|
|
#include "../src/serve/tool_calling.h"
|
|
#include "../src/serve/json_reader.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static const char *TOOLS_JSON =
|
|
"[{\"type\":\"function\",\"function\":{\"name\":\"read\","
|
|
"\"parameters\":{\"type\":\"object\",\"properties\":{"
|
|
"\"path\":{\"type\":\"string\",\"minLength\":1}},\"required\":[\"path\"]}}},"
|
|
"{\"type\":\"function\",\"function\":{\"name\":\"add\","
|
|
"\"parameters\":{\"type\":\"object\",\"properties\":{"
|
|
"\"a\":{\"type\":\"integer\"},\"b\":{\"type\":\"integer\"}},"
|
|
"\"required\":[\"a\",\"b\"]}}}]";
|
|
|
|
/* Toy vocab: real ornith fragments, reasoning text, and junk the grammar must
|
|
* refuse once the call is compelled. Id 0 is the end token (no text). */
|
|
static const char *VOCAB[] = {
|
|
"", "Let", " me", " think", "{", "</think>", "\n\n", "```json\n",
|
|
"{\"function\": \"read\"", ", \"arguments\": {", "\"path\": \"", "src",
|
|
"/main.c", "\"}}", "\n```", "Sure, here it is", " ", "\"", "```", "}",
|
|
"{\"function\": \"add\"", "\"a\": 1, \"b\": 2}}", "ok", "\n",
|
|
};
|
|
#define VOCAB_N ((int)(sizeof(VOCAB) / sizeof(VOCAB[0])))
|
|
#define END_ID 0
|
|
#define THINK_CLOSE_ID 5
|
|
#define TOP_N 4
|
|
#define MAX_SEQ 128
|
|
|
|
/* ---- grammar ops over tgj_state ---- */
|
|
static void ops_copy(void *d, const void *s) { tgj_state_copy(d, s); }
|
|
static bool ops_allowed(const void *st, const char *t) { return tgj_token_allowed(st, t); }
|
|
static void ops_advance(void *st, const char *t) { tgj_advance(st, t); }
|
|
static bool ops_mask(const void *st) { return tgj_mask_active(st); }
|
|
static bool ops_mask_never(const void *st) { (void)st; return false; }
|
|
static bool ops_complete(const void *st) { return tgj_complete(st); }
|
|
static const gsc_grammar_ops OPS = { ops_copy, ops_allowed, ops_advance, ops_mask, ops_complete };
|
|
static const gsc_grammar_ops OPS_IGNORING_GRAMMAR = {
|
|
ops_copy, ops_allowed, ops_advance, ops_mask_never, ops_complete };
|
|
|
|
static const char *vocab_text(void *ctx, uint32_t id) {
|
|
(void)ctx;
|
|
return id < (uint32_t)VOCAB_N ? VOCAB[id] : NULL;
|
|
}
|
|
|
|
/* ---- the toy model: logits are a hash of (salt, prefix) ---- */
|
|
static uint64_t mix64(uint64_t x) {
|
|
x += 0x9E3779B97F4A7C15ull;
|
|
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
|
|
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
|
|
return x ^ (x >> 31);
|
|
}
|
|
|
|
static void toy_logits(uint64_t salt, const uint32_t *seq, int n, float *out) {
|
|
uint64_t h = mix64(salt);
|
|
for (int i = 0; i < n; i++) h = mix64(h ^ seq[i]);
|
|
for (int v = 0; v < VOCAB_N; v++)
|
|
out[v] = (float)(mix64(h + (uint64_t)v) >> 40) / (float)(1u << 24);
|
|
/* Close the think block after a while, and favour the fragments that finish
|
|
* a call, so most trials reach a complete call inside MAX_SEQ. */
|
|
if (n >= 6 + (int)(h % 8)) out[THINK_CLOSE_ID] += 0.6f;
|
|
static const int CALL_FRAGMENTS[] = { 7, 8, 9, 10, 13, 14 };
|
|
for (int i = 0; i < (int)(sizeof(CALL_FRAGMENTS) / sizeof(CALL_FRAGMENTS[0])); i++)
|
|
out[CALL_FRAGMENTS[i]] += 0.3f;
|
|
out[END_ID] -= 0.5f; /* an early end token would make trials trivial */
|
|
}
|
|
|
|
/* Argmax, lowest id on a tie; under `walk`'s active mask only allowed ids. */
|
|
static uint32_t masked_argmax(const float *logits, const tgj_state *walk) {
|
|
bool masking = walk && tgj_mask_active(walk);
|
|
int best = -1;
|
|
for (int v = 0; v < VOCAB_N; v++) {
|
|
if (masking && VOCAB[v][0] && !tgj_token_allowed(walk, VOCAB[v])) continue;
|
|
if (best < 0 || logits[v] > logits[best]) best = v;
|
|
}
|
|
return (uint32_t)best;
|
|
}
|
|
|
|
/* The card's top-N (index, value) pairs of a dense row, in id order. */
|
|
static uint32_t top_n_pairs(const float *logits, uint32_t *index, float *value) {
|
|
bool taken[VOCAB_N] = { false };
|
|
for (int k = 0; k < TOP_N; k++) {
|
|
int best = -1;
|
|
for (int v = 0; v < VOCAB_N; v++)
|
|
if (!taken[v] && (best < 0 || logits[v] > logits[best])) best = v;
|
|
taken[best] = true;
|
|
}
|
|
uint32_t n = 0;
|
|
for (int v = 0; v < VOCAB_N; v++)
|
|
if (taken[v]) { index[n] = (uint32_t)v; value[n] = logits[v]; n++; }
|
|
return n;
|
|
}
|
|
|
|
/* Plain constrained decode. Returns the token count. */
|
|
static int decode_plain(const tgj_grammar *g, uint64_t salt, uint32_t *seq) {
|
|
tgj_state *walk = tgj_state_new_required(g, true);
|
|
float logits[VOCAB_N];
|
|
int n = 0;
|
|
while (n < MAX_SEQ) {
|
|
toy_logits(salt, seq, n, logits);
|
|
uint32_t tok = masked_argmax(logits, walk);
|
|
if (tok == END_ID) break;
|
|
seq[n++] = tok;
|
|
tgj_advance(walk, VOCAB[tok]);
|
|
if (tgj_complete(walk)) break;
|
|
}
|
|
tgj_state_free(walk);
|
|
return n;
|
|
}
|
|
|
|
/* A row under an active mask, decided as the serve does: from the row's top-N
|
|
* by the shortlist rule (greedy), else — row 0 only — the dense row at the
|
|
* cycle's start, masked; a later undecidable row defers. */
|
|
typedef struct {
|
|
uint64_t salt;
|
|
const uint32_t *seq;
|
|
int n;
|
|
unsigned long dense_rows;
|
|
} masked_ctx;
|
|
|
|
static bool walk_allows(void *ctx, uint32_t id) {
|
|
return !VOCAB[id][0] || tgj_token_allowed((const tgj_state *)ctx, VOCAB[id]);
|
|
}
|
|
|
|
static uint32_t masked_pick(void *ctx, void *walk, int row, const uint32_t *index,
|
|
const float *value, uint32_t count) {
|
|
masked_ctx *m = (masked_ctx *)ctx;
|
|
float sparse[VOCAB_N], logits[VOCAB_N];
|
|
uint32_t ids[GMS_MAX];
|
|
float bound;
|
|
gms_sampling greedy = { .temperature = 0.0f, .top_k = 1, .repeat_penalty = 1.0f };
|
|
uint32_t neg = 0xff800000u;
|
|
for (int v = 0; v < VOCAB_N; v++) memcpy(&sparse[v], &neg, sizeof(float));
|
|
for (uint32_t i = 0; i < count; i++) sparse[index[i]] = value[i];
|
|
int n = gms_select_shortlist(sparse, VOCAB_N, ids, &bound);
|
|
if (gms_mask_from_shortlist(sparse, VOCAB_N, ids, n, bound, &greedy, walk_allows, walk)) {
|
|
int best = -1;
|
|
for (int v = 0; v < VOCAB_N; v++)
|
|
if (sparse[v] > -1e30f && (best < 0 || sparse[v] > sparse[best])) best = v;
|
|
return (uint32_t)best;
|
|
}
|
|
if (row > 0) return GSC_DEFER;
|
|
m->dense_rows++;
|
|
toy_logits(m->salt, m->seq, m->n, logits);
|
|
return masked_argmax(logits, (const tgj_state *)walk);
|
|
}
|
|
|
|
/* Drafts: the true plain continuation, with junk mixed in by the hash. */
|
|
static int make_drafts(const tgj_grammar *g, uint64_t salt, const uint32_t *seq, int n,
|
|
uint32_t *drafts) {
|
|
uint32_t truth[MAX_SEQ + 4];
|
|
int nt = decode_plain(g, salt, truth);
|
|
int nd = 0;
|
|
uint64_t h = mix64(salt ^ (uint64_t)n * 7919u);
|
|
for (int j = 0; j < 3; j++) {
|
|
h = mix64(h);
|
|
uint32_t d = (h % 3 == 0 || n + j >= nt) ? (uint32_t)(h >> 33) % VOCAB_N
|
|
: truth[n + j];
|
|
if (d == END_ID) break; /* the serve cuts drafts at an end token */
|
|
drafts[nd++] = d;
|
|
}
|
|
(void)seq;
|
|
return nd;
|
|
}
|
|
|
|
/* One speculative verify cycle, accepting exactly as spec_step_ex does. */
|
|
static int verify_cycle(gsc_chooser *c, uint64_t salt, uint32_t *seq, int n,
|
|
const uint32_t *drafts, int nd, uint32_t *emit) {
|
|
uint32_t index[TOP_N];
|
|
float value[TOP_N], logits[VOCAB_N];
|
|
uint32_t row_seq[MAX_SEQ + 8];
|
|
int n_accept = 0;
|
|
uint32_t next;
|
|
memcpy(row_seq, seq, (size_t)n * sizeof(*seq));
|
|
gsc_begin_cycle(c);
|
|
for (;;) {
|
|
for (int i = 0; i < n_accept; i++) row_seq[n + i] = drafts[i];
|
|
toy_logits(salt, row_seq, n + n_accept, logits);
|
|
uint32_t cnt = top_n_pairs(logits, index, value);
|
|
uint32_t tok = gsc_choose(c, index, value, cnt);
|
|
if (tok == GSC_DEFER) {
|
|
if (n_accept == 0) return -1; /* spec_step_ex fails the cycle */
|
|
n_accept--;
|
|
next = drafts[n_accept];
|
|
break;
|
|
}
|
|
if (n_accept < nd && tok == drafts[n_accept]) { n_accept++; continue; }
|
|
next = tok;
|
|
break;
|
|
}
|
|
for (int i = 0; i < n_accept; i++) emit[i] = drafts[i];
|
|
emit[n_accept] = next;
|
|
return n_accept + 1;
|
|
}
|
|
|
|
/* Speculative constrained decode through the chooser. Returns the count. */
|
|
static int decode_spec(const tgj_grammar *g, uint64_t salt, const gsc_grammar_ops *ops,
|
|
uint32_t *seq, unsigned long *cycles) {
|
|
tgj_state *live = tgj_state_new_required(g, true);
|
|
tgj_state *walk = tgj_state_clone(live);
|
|
masked_ctx dctx = { salt, seq, 0, 0 };
|
|
gsc_chooser c = { .ops = ops, .live = live, .walk = walk, .text = vocab_text,
|
|
.masked = masked_pick, .masked_ctx = &dctx };
|
|
float logits[VOCAB_N];
|
|
int n = 0;
|
|
bool stop = false;
|
|
while (n < MAX_SEQ && !stop) {
|
|
uint32_t drafts[4], emit[8];
|
|
int nd = make_drafts(g, salt, seq, n, drafts);
|
|
nd = gsc_truncate_drafts(&c, drafts, nd);
|
|
int ne;
|
|
if (n == 0 || nd < 1) { /* no cycle: one plain step */
|
|
toy_logits(salt, seq, n, logits);
|
|
emit[0] = masked_argmax(logits, ops == &OPS ? live : NULL);
|
|
ne = 1;
|
|
} else {
|
|
dctx.n = n;
|
|
ne = verify_cycle(&c, salt, seq, n, drafts, nd, emit);
|
|
(*cycles)++;
|
|
if (ne < 0) { /* a refused cycle: report a divergence */
|
|
seq[n++] = END_ID;
|
|
break;
|
|
}
|
|
}
|
|
for (int i = 0; i < ne && !stop; i++) {
|
|
if (emit[i] == END_ID || n >= MAX_SEQ) { stop = true; break; }
|
|
seq[n++] = emit[i];
|
|
tgj_advance(live, VOCAB[emit[i]]);
|
|
if (tgj_complete(live)) stop = true;
|
|
}
|
|
}
|
|
tgj_state_free(walk);
|
|
tgj_state_free(live);
|
|
return n;
|
|
}
|
|
|
|
/* The text the model emitted after its think block. */
|
|
static void content_after_think(const uint32_t *seq, int n, char *out, size_t cap) {
|
|
size_t len = 0;
|
|
int start = 0;
|
|
for (int i = 0; i < n; i++)
|
|
if (seq[i] == THINK_CLOSE_ID) { start = i + 1; break; }
|
|
out[0] = '\0';
|
|
for (int i = start; i < n; i++) {
|
|
size_t t = strlen(VOCAB[seq[i]]);
|
|
if (len + t + 1 >= cap) break;
|
|
memcpy(out + len, VOCAB[seq[i]], t);
|
|
len += t;
|
|
out[len] = '\0';
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
* Spec == plain over many trials; completed calls parse.
|
|
* ============================================================ */
|
|
static void test_spec_equals_plain_constrained(void) {
|
|
jr_value *tools = jr_parse(TOOLS_JSON);
|
|
tgj_grammar *g = tgj_build(tools);
|
|
int mismatches = 0, completed = 0, parsed = 0, ignoring_mismatches = 0;
|
|
unsigned long cycles = 0;
|
|
for (uint64_t salt = 1; salt <= 400; salt++) {
|
|
uint32_t plain[MAX_SEQ + 8], spec[MAX_SEQ + 8], loose[MAX_SEQ + 8];
|
|
int np = decode_plain(g, salt, plain);
|
|
int ns = decode_spec(g, salt, &OPS, spec, &cycles);
|
|
int nl = decode_spec(g, salt, &OPS_IGNORING_GRAMMAR, loose, &cycles);
|
|
if (np != ns || memcmp(plain, spec, (size_t)np * sizeof(*plain)) != 0) mismatches++;
|
|
if (np != nl || memcmp(plain, loose, (size_t)np * sizeof(*plain)) != 0)
|
|
ignoring_mismatches++;
|
|
tgj_state *w = tgj_state_new_required(g, true);
|
|
for (int i = 0; i < np; i++) tgj_advance(w, VOCAB[plain[i]]);
|
|
if (tgj_complete(w)) {
|
|
char text[4096];
|
|
tc_tool_call *calls = NULL;
|
|
int n_calls = 0;
|
|
completed++;
|
|
content_after_think(plain, np, text, sizeof(text));
|
|
if (tc_parse_tool_calls(text, tools, TC_DIALECT_ORNITH_JSON, &calls, &n_calls) == 0 &&
|
|
n_calls == 1 && tc_tool_name_known(tools, calls[0].name))
|
|
parsed++;
|
|
tc_tool_calls_free(calls, n_calls);
|
|
}
|
|
tgj_state_free(w);
|
|
}
|
|
printf(" trials=400 cycles=%lu completed=%d parsed=%d mismatches=%d "
|
|
"grammar-ignoring mismatches=%d\n", cycles, completed, parsed, mismatches,
|
|
ignoring_mismatches);
|
|
if (ignoring_mismatches == 0) {
|
|
printf("VACUOUS: ignoring the grammar never changed the output\n");
|
|
exit(2);
|
|
}
|
|
ASSERT_INT_EQ(mismatches, 0);
|
|
ASSERT_TRUE(completed > 100);
|
|
ASSERT_INT_EQ(parsed, completed);
|
|
tgj_free(g);
|
|
jr_free(tools);
|
|
}
|
|
|
|
/* ============================================================
|
|
* Truncation: a grammar-breaking draft is cut, and nothing follows completion.
|
|
* ============================================================ */
|
|
static void test_truncate_drafts(void) {
|
|
jr_value *tools = jr_parse(TOOLS_JSON);
|
|
tgj_grammar *g = tgj_build(tools);
|
|
tgj_state *live = tgj_state_new_required(g, true);
|
|
tgj_state *walk = tgj_state_clone(live);
|
|
gsc_chooser c = { .ops = &OPS, .live = live, .walk = walk, .text = vocab_text };
|
|
|
|
/* Reasoning: anything goes, '{' included. */
|
|
uint32_t think[3] = { 4, 15, 18 };
|
|
ASSERT_INT_EQ(gsc_truncate_drafts(&c, think, 3), 3);
|
|
|
|
tgj_advance(live, "</think>");
|
|
tgj_advance(live, "\n\n");
|
|
uint32_t prose[3] = { 7, 15, 8 }; /* fence, then junk */
|
|
ASSERT_INT_EQ(gsc_truncate_drafts(&c, prose, 3), 1);
|
|
uint32_t good[3] = { 7, 8, 9 };
|
|
ASSERT_INT_EQ(gsc_truncate_drafts(&c, good, 3), 3);
|
|
|
|
tgj_advance(live, "```json\n");
|
|
tgj_advance(live, "{\"function\": \"read\"");
|
|
tgj_advance(live, ", \"arguments\": {");
|
|
uint32_t finish[4] = { 10, 11, 13, 14 }; /* path, value, close, fence close */
|
|
ASSERT_INT_EQ(gsc_truncate_drafts(&c, finish, 4), 4);
|
|
uint32_t past[3] = { 10, 13, 14 }; /* "": an empty path is refused */
|
|
ASSERT_INT_EQ(gsc_truncate_drafts(&c, past, 3), 1);
|
|
tgj_advance(live, "\"path\": \"");
|
|
tgj_advance(live, "src");
|
|
uint32_t over[3] = { 13, 14, 22 }; /* complete after 14 → 22 cut */
|
|
ASSERT_INT_EQ(gsc_truncate_drafts(&c, over, 3), 2);
|
|
ASSERT_TRUE(c.drafts_cut >= 4);
|
|
|
|
tgj_state_free(walk);
|
|
tgj_state_free(live);
|
|
tgj_free(g);
|
|
jr_free(tools);
|
|
}
|
|
|
|
/* Routing: an unmasked row goes to the base chooser, a masked row to the masked
|
|
* pick (with the private walk and the row index), and the masked pick's DEFER
|
|
* ends the cycle. */
|
|
static int g_base_calls, g_masked_calls, g_masked_last_row;
|
|
static uint32_t base_first(void *ctx, int row, const uint32_t *index, const float *value,
|
|
uint32_t count) {
|
|
(void)ctx; (void)row; (void)value; (void)count;
|
|
g_base_calls++;
|
|
return index[0];
|
|
}
|
|
static uint32_t masked_fence_then_defer(void *ctx, void *walk, int row, const uint32_t *index,
|
|
const float *value, uint32_t count) {
|
|
(void)ctx; (void)index; (void)value; (void)count;
|
|
g_masked_calls++;
|
|
g_masked_last_row = row;
|
|
return tgj_in_call((const tgj_state *)walk) ? GSC_DEFER : 7u; /* fence, then defer */
|
|
}
|
|
|
|
static void test_rows_route_by_mask(void) {
|
|
jr_value *tools = jr_parse(TOOLS_JSON);
|
|
tgj_grammar *g = tgj_build(tools);
|
|
tgj_state *live = tgj_state_new_required(g, true);
|
|
tgj_state *walk = tgj_state_clone(live);
|
|
gsc_chooser c = { .ops = &OPS, .live = live, .walk = walk, .text = vocab_text,
|
|
.base = base_first, .masked = masked_fence_then_defer };
|
|
uint32_t idx_close[2] = { 5, 22 }; /* "</think>" first */
|
|
uint32_t idx_any[2] = { 15, 22 };
|
|
float val[2] = { 1.0f, 0.5f };
|
|
|
|
g_base_calls = g_masked_calls = 0;
|
|
gsc_begin_cycle(&c);
|
|
ASSERT_TRUE(gsc_choose(&c, idx_close, val, 2) == 5u); /* reasoning: base */
|
|
ASSERT_INT_EQ(g_base_calls, 1);
|
|
ASSERT_TRUE(gsc_choose(&c, idx_any, val, 2) == 7u); /* compelled: masked pick */
|
|
ASSERT_INT_EQ(g_masked_calls, 1);
|
|
ASSERT_INT_EQ(g_masked_last_row, 1);
|
|
ASSERT_TRUE(gsc_choose(&c, idx_any, val, 2) == GSC_DEFER);
|
|
ASSERT_INT_EQ(g_masked_last_row, 2);
|
|
ASSERT_INT_EQ(g_base_calls, 1);
|
|
|
|
tgj_state_free(walk);
|
|
tgj_state_free(live);
|
|
tgj_free(g);
|
|
jr_free(tools);
|
|
}
|
|
|
|
/* Nothing past the call is committed: the row after the one that completed
|
|
* the call defers, so the completing token ends the cycle uncommitted. */
|
|
static void test_defer_after_completion(void) {
|
|
jr_value *tools = jr_parse(TOOLS_JSON);
|
|
tgj_grammar *g = tgj_build(tools);
|
|
tgj_state *live = tgj_state_new_required(g, true);
|
|
const char *frags[] = { "</think>", "```json\n", "{\"function\": \"read\"",
|
|
", \"arguments\": {", "\"path\": \"", "src", "\"}}" };
|
|
for (int i = 0; i < 7; i++) tgj_advance(live, frags[i]);
|
|
tgj_state *walk = tgj_state_clone(live);
|
|
masked_ctx m = { 0, NULL, 0, 0 };
|
|
gsc_chooser c = { .ops = &OPS, .live = live, .walk = walk, .text = vocab_text,
|
|
.masked = masked_pick, .masked_ctx = &m };
|
|
uint32_t idx[2] = { 14, 22 };
|
|
float val[2] = { 2.0f, 1.0f };
|
|
gsc_begin_cycle(&c);
|
|
ASSERT_TRUE(gsc_choose(&c, idx, val, 2) == 14u); /* the fence close completes it */
|
|
ASSERT_TRUE(c.completed);
|
|
ASSERT_TRUE(gsc_choose(&c, idx, val, 2) == GSC_DEFER);
|
|
tgj_state_free(walk);
|
|
tgj_state_free(live);
|
|
tgj_free(g);
|
|
jr_free(tools);
|
|
}
|
|
|
|
int main(void) {
|
|
printf("\n=== grammar_spec_chooser (speculative decoding under the grammar) ===\n\n");
|
|
RUN_TEST(truncate_drafts);
|
|
RUN_TEST(rows_route_by_mask);
|
|
RUN_TEST(defer_after_completion);
|
|
RUN_TEST(spec_equals_plain_constrained);
|
|
TEST_REPORT();
|
|
return __ft_failed == 0 ? 0 : 1;
|
|
}
|