Scaffolding: ZMK module structure, Kconfig, DTS, behavior driver

- zephyr/module.yml with dts_root for dt-bindings
- Kconfig: STENO_ENGINE, dict selection (Plover/Lapwing/test),
  MPHF toggle, Unicode modes, history size, multi-stroke timeout
- DTS behavior binding (one_param, steno key index)
- behavior_steno.c: chord accumulation, all-up detection,
  multi-stroke buffering with timeout, star undo, formatter pipeline,
  3-way dict dispatch (split/MPHF/simple trie)
- dict_embed.S: .incbin from generated header path
- steno_keys.h: 23-key layout, bit positions matching compiler
This commit is contained in:
afiqzudinhadi 2026-07-02 02:04:28 +08:00
parent 234bc8d731
commit 1ec204b845
33 changed files with 5443 additions and 0 deletions

64
tests/run_tests.sh Executable file
View file

@ -0,0 +1,64 @@
#!/bin/bash
# Run MPHF dictionary tests: compile test dict, build C test, execute.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
mkdir -p "$BUILD_DIR"
echo "=== Step 1: Create test dictionary JSON ==="
cat > "$BUILD_DIR/test_dict.json" << 'DICTEOF'
{
"S": "is",
"T": "it",
"THE": "the",
"KAT": "cat",
"TK": "did",
"SKP": "and",
"TPOR": "for",
"STO": "so",
"HAOEU": "hi",
"TKOGS": "dogs",
"S/T": "{.}",
"PHAO*EUP/HRAOEUPB": "my line"
}
DICTEOF
echo "=== Step 2: Compile test dictionary ==="
python3 "$ROOT_DIR/tools/compile_mphf.py" \
"$BUILD_DIR/test_dict.json" \
"$BUILD_DIR/test_dict.bin" \
--stats
echo ""
echo "=== Step 3: Build C test binary ==="
cc -O2 -Wall -Wextra -I"$ROOT_DIR/src" \
-o "$BUILD_DIR/test_mphf" \
"$SCRIPT_DIR/test_mphf.c" \
"$ROOT_DIR/src/dict_mphf.c"
echo ""
echo "=== Step 4: Run tests ==="
"$BUILD_DIR/test_mphf" "$BUILD_DIR/test_dict.bin"
echo ""
echo "=== Step 5: Run with larger dict (if Plover available) ==="
PLOVER="/tmp/plover-main.json"
if [ -f "$PLOVER" ]; then
echo "Compiling 1000-entry subset..."
python3 "$ROOT_DIR/tools/compile_mphf.py" \
"$PLOVER" \
"$BUILD_DIR/plover_1k.bin" \
--max-entries 1000 \
--stats
echo ""
echo "Binary size: $(wc -c < "$BUILD_DIR/plover_1k.bin") bytes"
else
echo "Plover dict not found at $PLOVER, skipping large dict test"
echo "Download: curl -sL 'https://raw.githubusercontent.com/openstenoproject/plover/main/plover/assets/main.json' -o /tmp/plover-main.json"
fi
echo ""
echo "=== Done ==="

340
tests/test_formatter.c Normal file
View file

@ -0,0 +1,340 @@
/*
* Native host test for Plover formatting engine.
* Build: cc -I../src -o test_formatter test_formatter.c ../src/formatter.c
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "formatter.h"
static int tests_run = 0;
static int tests_passed = 0;
#define ASSERT_EQ_STR(actual, expected, msg) do { \
tests_run++; \
if (strcmp((actual), (expected)) == 0) { \
tests_passed++; \
} else { \
printf("FAIL [%s]: got \"%s\", expected \"%s\"\n", msg, actual, expected); \
} \
} while (0)
#define ASSERT_EQ_INT(actual, expected, msg) do { \
tests_run++; \
if ((actual) == (expected)) { \
tests_passed++; \
} else { \
printf("FAIL [%s]: got %d, expected %d\n", msg, (int)(actual), (int)(expected)); \
} \
} while (0)
#define ASSERT_TRUE(cond, msg) do { \
tests_run++; \
if ((cond)) { \
tests_passed++; \
} else { \
printf("FAIL [%s]\n", msg); \
} \
} while (0)
/* Helper: process and return output text */
static struct steno_fmt_result proc(struct steno_fmt_state *s, const char *t)
{
struct steno_fmt_result r;
steno_fmt_process(s, t, &r);
return r;
}
static void test_raw_text(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
/* First word: no space before */
struct steno_fmt_result r = proc(&s, "hello");
ASSERT_EQ_STR(r.text, "hello", "raw: first word");
ASSERT_EQ_INT(r.len, 5, "raw: first word len");
/* Second word: space before */
r = proc(&s, "world");
ASSERT_EQ_STR(r.text, " world", "raw: second word with space");
ASSERT_EQ_INT(r.len, 6, "raw: second word len");
}
static void test_attach(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "walk");
struct steno_fmt_result r = proc(&s, "{^}");
ASSERT_TRUE(r.is_command_only || r.len == 0, "attach: no output");
r = proc(&s, "ing");
ASSERT_EQ_STR(r.text, "ing", "attach: no space after {^}");
}
static void test_suffix_attach(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "walk");
struct steno_fmt_result r = proc(&s, "{^ing}");
ASSERT_EQ_STR(r.text, "ing", "suffix: attached");
ASSERT_EQ_INT(r.len, 3, "suffix: len");
}
static void test_prefix_attach(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
struct steno_fmt_result r = proc(&s, "{pre^}");
ASSERT_EQ_STR(r.text, "pre", "prefix: text");
r = proc(&s, "fix");
ASSERT_EQ_STR(r.text, "fix", "prefix: next word attached");
}
static void test_capitalize_next(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "{-|}");
struct steno_fmt_result r = proc(&s, "hello");
ASSERT_EQ_STR(r.text, "Hello", "cap_next: first word capitalized");
/* Verify cap_next is one-shot */
r = proc(&s, "world");
ASSERT_EQ_STR(r.text, " world", "cap_next: one-shot reset");
}
static void test_punctuation(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "hello");
struct steno_fmt_result r = proc(&s, "{.}");
ASSERT_EQ_STR(r.text, ".", "period: attached");
/* Next word should be capitalized */
r = proc(&s, "world");
ASSERT_EQ_STR(r.text, " World", "period: cap next");
}
static void test_comma(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "hello");
struct steno_fmt_result r = proc(&s, "{,}");
ASSERT_EQ_STR(r.text, ",", "comma: attached");
/* Comma does NOT capitalize next */
r = proc(&s, "world");
ASSERT_EQ_STR(r.text, " world", "comma: no cap next");
}
static void test_sentence_flow(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
struct steno_fmt_result r;
r = proc(&s, "I");
ASSERT_EQ_STR(r.text, "I", "sentence: I");
r = proc(&s, "{.}");
ASSERT_EQ_STR(r.text, ".", "sentence: period");
r = proc(&s, "the");
ASSERT_EQ_STR(r.text, " The", "sentence: The after period");
}
static void test_mode_caps(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "{MODE:CAPS}");
struct steno_fmt_result r = proc(&s, "hello");
ASSERT_EQ_STR(r.text, "HELLO", "mode_caps: uppercase");
r = proc(&s, "world");
ASSERT_EQ_STR(r.text, " WORLD", "mode_caps: persists");
proc(&s, "{MODE:RESET}");
r = proc(&s, "test");
ASSERT_EQ_STR(r.text, " test", "mode_reset: normal");
}
static void test_mode_title(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "{MODE:TITLE}");
struct steno_fmt_result r = proc(&s, "hello");
ASSERT_EQ_STR(r.text, "Hello", "mode_title: capitalize");
proc(&s, "{MODE:RESET}");
}
static void test_mode_lower(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "{MODE:LOWER}");
struct steno_fmt_result r = proc(&s, "HELLO");
ASSERT_EQ_STR(r.text, "hello", "mode_lower: lowercase");
proc(&s, "{MODE:RESET}");
}
static void test_fingerspelling(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
struct steno_fmt_result r;
r = proc(&s, "{&a}");
ASSERT_EQ_STR(r.text, "a", "finger: a");
r = proc(&s, "{&b}");
ASSERT_EQ_STR(r.text, "b", "finger: b glued");
r = proc(&s, "{&c}");
ASSERT_EQ_STR(r.text, "c", "finger: c glued");
/* Non-fingerspelling should get space */
r = proc(&s, "hello");
ASSERT_EQ_STR(r.text, " hello", "finger: break with space");
}
static void test_combined_commands(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "test");
/* {^}{-|} — suppress space + capitalize */
struct steno_fmt_result r = proc(&s, "{^}{-|}");
ASSERT_TRUE(r.len == 0 || r.is_command_only, "combined: no text");
r = proc(&s, "word");
ASSERT_EQ_STR(r.text, "Word", "combined: attached + capitalized");
}
static void test_undo(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
struct steno_fmt_result r = proc(&s, "{*}");
ASSERT_TRUE(r.is_undo, "undo: flag set");
ASSERT_TRUE(r.is_command_only, "undo: command only");
}
static void test_key_combo(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
struct steno_fmt_result r = proc(&s, "{#Return}");
ASSERT_TRUE(r.is_command_only, "key_combo: command only");
}
static void test_literal_braces(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
struct steno_fmt_result r = proc(&s, "\\{");
ASSERT_EQ_STR(r.text, "{", "literal: left brace");
r = proc(&s, "\\}");
ASSERT_EQ_STR(r.text, " }", "literal: right brace with space");
}
static void test_uppercase_next(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "{<}");
struct steno_fmt_result r = proc(&s, "hello");
ASSERT_EQ_STR(r.text, "HELLO", "upper_next: entire word");
/* One-shot */
r = proc(&s, "world");
ASSERT_EQ_STR(r.text, " world", "upper_next: one-shot");
}
static void test_lowercase_next(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
proc(&s, "{>}");
struct steno_fmt_result r = proc(&s, "HELLO");
ASSERT_EQ_STR(r.text, "hello", "lower_next: entire word");
}
static void test_empty_translation(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
struct steno_fmt_result r = proc(&s, "");
ASSERT_TRUE(r.is_command_only, "empty: command only");
ASSERT_EQ_INT(r.len, 0, "empty: no text");
}
static void test_prefix_then_suffix(void)
{
struct steno_fmt_state s;
steno_fmt_init(&s);
/* {re^} then {^ed} */
proc(&s, "{re^}");
struct steno_fmt_result r = proc(&s, "{^ed}");
ASSERT_EQ_STR(r.text, "ed", "pre+suf: attached");
}
int main(void)
{
test_raw_text();
test_attach();
test_suffix_attach();
test_prefix_attach();
test_capitalize_next();
test_punctuation();
test_comma();
test_sentence_flow();
test_mode_caps();
test_mode_title();
test_mode_lower();
test_fingerspelling();
test_combined_commands();
test_undo();
test_key_combo();
test_literal_braces();
test_uppercase_next();
test_lowercase_next();
test_empty_translation();
test_prefix_then_suffix();
printf("\n%d/%d tests passed\n", tests_passed, tests_run);
if (tests_passed == tests_run) {
printf("ALL TESTS PASSED\n");
return 0;
}
return 1;
}

383
tests/test_mphf.c Normal file
View file

@ -0,0 +1,383 @@
/**
* Native tests for MPHF dictionary engine.
*
* Builds and runs on host (not ZMK). Compiles a small test dictionary
* via compile_mphf.py, then exercises all lookup paths in C.
*
* Build: cc -O2 -I../src -o test_mphf test_mphf.c ../src/dict_mphf.c
* Run: ./test_mphf test_dict.bin
*
* SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "dict_mphf.h"
/* ─── Test helpers ─── */
static int tests_run = 0;
static int tests_passed = 0;
#define TEST(name) \
do { printf(" %-50s ", #name); tests_run++; } while(0)
#define PASS() \
do { tests_passed++; printf("PASS\n"); } while(0)
#define FAIL(msg) \
do { printf("FAIL: %s\n", msg); } while(0)
#define ASSERT_EQ_INT(a, b) \
do { \
if ((a) != (b)) { \
char _buf[128]; \
snprintf(_buf, sizeof(_buf), "expected %d, got %d", (int)(b), (int)(a)); \
FAIL(_buf); return; \
} \
} while(0)
#define ASSERT_EQ_STR(a, b) \
do { \
if (strcmp((a), (b)) != 0) { \
char _buf[256]; \
snprintf(_buf, sizeof(_buf), "expected \"%s\", got \"%s\"", (b), (a)); \
FAIL(_buf); return; \
} \
} while(0)
#define ASSERT_NULL(a) \
do { \
if ((a) != NULL) { \
FAIL("expected NULL"); return; \
} \
} while(0)
#define ASSERT_NOT_NULL(a) \
do { \
if ((a) == NULL) { \
FAIL("expected non-NULL"); return; \
} \
} while(0)
#define ASSERT_TRUE(a) \
do { \
if (!(a)) { \
FAIL("expected true"); return; \
} \
} while(0)
#define ASSERT_FALSE(a) \
do { \
if ((a)) { \
FAIL("expected false"); return; \
} \
} while(0)
/* ─── Load compiled binary from file ─── */
static uint8_t *load_file(const char *path, size_t *out_len)
{
FILE *f = fopen(path, "rb");
if (!f) {
fprintf(stderr, "Cannot open %s\n", path);
return NULL;
}
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *data = malloc(len);
if (!data) {
fclose(f);
return NULL;
}
fread(data, 1, len, f);
fclose(f);
*out_len = (size_t)len;
return data;
}
/* ─── Test: init with valid data ─── */
static void test_init_valid(const uint8_t *data, size_t len)
{
TEST(init_valid);
struct dict_mphf dict;
int rc = dict_mphf_init(&dict, data, len);
ASSERT_EQ_INT(rc, 0);
ASSERT_EQ_INT(dict.header->magic, DICT_MPHF_MAGIC);
ASSERT_EQ_INT(dict.header->version, DICT_MPHF_VERSION);
PASS();
}
/* ─── Test: init with NULL ─── */
static void test_init_null(void)
{
TEST(init_null);
struct dict_mphf dict;
ASSERT_EQ_INT(dict_mphf_init(&dict, NULL, 0), -1);
ASSERT_EQ_INT(dict_mphf_init(NULL, &dict, 32), -1);
PASS();
}
/* ─── Test: init with truncated data ─── */
static void test_init_truncated(const uint8_t *data)
{
TEST(init_truncated);
struct dict_mphf dict;
ASSERT_EQ_INT(dict_mphf_init(&dict, data, 16), -2);
PASS();
}
/* ─── Test: init with bad magic ─── */
static void test_init_bad_magic(void)
{
TEST(init_bad_magic);
uint8_t bad[32] = {0};
struct dict_mphf dict;
ASSERT_EQ_INT(dict_mphf_init(&dict, bad, sizeof(bad)), -3);
PASS();
}
/* ─── Test: lookup known entries ─── */
/*
* The test dictionary (generated by test runner script) contains:
* "S" "is"
* "T" "it"
* "THE" "the"
* "KAT" "cat"
* "TK" "did"
* "SKP" "and"
* "TPOR" "for"
* "STO" "so"
* "HAOEU" "hi"
* "TKOGS" "dogs"
* "S/T" "{.}" (multi-stroke)
* "PHAO*EUP/HRAOEUPB" "my line" (multi-stroke)
*/
/* Steno key values (must match Python parser) */
#define SK_S 0x00000001u
#define SK_T 0x00000002u
#define SK_K 0x00000004u
#define SK_P 0x00000008u
#define SK_W 0x00000010u
#define SK_H 0x00000020u
#define SK_R 0x00000040u
#define SK_A 0x00000080u
#define SK_O 0x00000100u
#define SK_STAR 0x00000200u
#define SK_E 0x00000400u
#define SK_U 0x00000800u
#define SK_rF 0x00001000u
#define SK_rR 0x00002000u
#define SK_rP 0x00004000u
#define SK_rB 0x00008000u
#define SK_rL 0x00010000u
#define SK_rG 0x00020000u
#define SK_rT 0x00040000u
#define SK_rS 0x00080000u
#define SK_rD 0x00100000u
#define SK_rZ 0x00200000u
#define SK_NUM 0x00400000u
static void test_lookup_single_S(const struct dict_mphf *dict)
{
TEST(lookup_single_S);
uint32_t strokes[] = { SK_S };
const char *result = dict_mphf_lookup(dict, strokes, 1);
ASSERT_NOT_NULL(result);
ASSERT_EQ_STR(result, "is");
PASS();
}
static void test_lookup_single_T(const struct dict_mphf *dict)
{
TEST(lookup_single_T);
uint32_t strokes[] = { SK_T };
const char *result = dict_mphf_lookup(dict, strokes, 1);
ASSERT_NOT_NULL(result);
ASSERT_EQ_STR(result, "it");
PASS();
}
static void test_lookup_THE(const struct dict_mphf *dict)
{
TEST(lookup_THE);
/* -T → "the" (right T = bit 18 = 0x00040000) */
uint32_t strokes[] = { SK_rT };
const char *result = dict_mphf_lookup(dict, strokes, 1);
ASSERT_NOT_NULL(result);
ASSERT_EQ_STR(result, "the");
PASS();
}
static void test_lookup_KAT(const struct dict_mphf *dict)
{
TEST(lookup_KAT);
uint32_t strokes[] = { SK_K | SK_A | SK_rT };
const char *result = dict_mphf_lookup(dict, strokes, 1);
ASSERT_NOT_NULL(result);
ASSERT_EQ_STR(result, "cat");
PASS();
}
static void test_lookup_TPOR(const struct dict_mphf *dict)
{
TEST(lookup_TPOR);
uint32_t strokes[] = { SK_T | SK_P | SK_O | SK_rR };
const char *result = dict_mphf_lookup(dict, strokes, 1);
ASSERT_NOT_NULL(result);
ASSERT_EQ_STR(result, "for");
PASS();
}
static void test_lookup_HAOEU(const struct dict_mphf *dict)
{
TEST(lookup_HAOEU);
uint32_t strokes[] = { SK_H | SK_A | SK_O | SK_E | SK_U };
const char *result = dict_mphf_lookup(dict, strokes, 1);
ASSERT_NOT_NULL(result);
ASSERT_EQ_STR(result, "high");
PASS();
}
/* ─── Test: lookup multi-stroke ─── */
static void test_lookup_multi_stroke(const struct dict_mphf *dict)
{
TEST(lookup_multi_stroke);
/* Multi-stroke entries may or may not be present depending on dict size.
* Just verify no crash on 2-stroke lookup. */
uint32_t strokes[] = { SK_S, SK_T };
const char *result = dict_mphf_lookup(dict, strokes, 2);
(void)result;
PASS();
}
/* ─── Test: lookup not found ─── */
static void test_lookup_not_found(const struct dict_mphf *dict)
{
TEST(lookup_not_found);
/* "Z" not in dictionary */
uint32_t strokes[] = { SK_rZ };
const char *result = dict_mphf_lookup(dict, strokes, 1);
/* Could be NULL (fingerprint mismatch) or a wrong string (false positive).
* Fingerprint gives 99.6% true-negative rate. For testing purposes,
* just ensure no crash. If NULL, great. If non-NULL, it's a known
* false positive from the 8-bit fingerprint. */
(void)result;
PASS();
}
/* ─── Test: lookup with zero strokes ─── */
static void test_lookup_zero_strokes(const struct dict_mphf *dict)
{
TEST(lookup_zero_strokes);
uint32_t strokes[] = { 0 };
const char *result = dict_mphf_lookup(dict, strokes, 0);
ASSERT_NULL(result);
PASS();
}
/* ─── Test: has_prefix ─── */
static void test_has_prefix_S(const struct dict_mphf *dict)
{
TEST(has_prefix_S);
/* prefix check depends on whether multi-stroke entries exist in dict.
* Just verify no crash. */
(void)dict_mphf_has_prefix(dict, SK_S);
PASS();
}
static void test_has_prefix_not_found(const struct dict_mphf *dict)
{
TEST(has_prefix_not_found);
/* Extremely unlikely stroke combo — should not be a prefix */
ASSERT_FALSE(dict_mphf_has_prefix(dict, 0x003FFFFFu));
PASS();
}
/* ─── Test: entry count ─── */
static void test_entry_count(const struct dict_mphf *dict)
{
TEST(entry_count);
uint32_t count = dict_mphf_count(dict);
ASSERT_TRUE(count > 0);
PASS();
}
/* ─── Main ─── */
int main(int argc, char **argv)
{
if (argc < 2) {
fprintf(stderr, "Usage: %s <compiled-dict.bin>\n", argv[0]);
return 1;
}
size_t len;
uint8_t *data = load_file(argv[1], &len);
if (!data) {
return 1;
}
printf("Loaded %zu bytes from %s\n\n", len, argv[1]);
/* Init tests */
test_init_valid(data, len);
test_init_null();
test_init_truncated(data);
test_init_bad_magic();
/* Init dict for remaining tests */
struct dict_mphf dict;
int rc = dict_mphf_init(&dict, data, len);
if (rc != 0) {
fprintf(stderr, "dict_mphf_init failed: %d\n", rc);
free(data);
return 1;
}
printf("\n Dict: %u entries, %u buckets, %u unique strings\n",
dict.header->entry_count, dict.header->bucket_count,
dict.header->unique_count);
printf(" Bits: disp=%u value=%u\n",
dict.header->disp_bits, dict.header->value_bits);
printf(" Prefixes: %u\n\n", dict.header->prefix_count);
/* Lookup tests */
test_lookup_single_S(&dict);
test_lookup_single_T(&dict);
test_lookup_THE(&dict);
test_lookup_KAT(&dict);
test_lookup_TPOR(&dict);
test_lookup_HAOEU(&dict);
test_lookup_multi_stroke(&dict);
test_lookup_not_found(&dict);
test_lookup_zero_strokes(&dict);
/* Prefix tests */
test_has_prefix_S(&dict);
test_has_prefix_not_found(&dict);
/* Entry count */
test_entry_count(&dict);
printf("\n%d/%d tests passed\n", tests_passed, tests_run);
free(data);
return tests_passed == tests_run ? 0 : 1;
}

102
tests/test_trie.c Normal file
View file

@ -0,0 +1,102 @@
/*
* Native test for trie.c run on host, not on target.
* Build: cc -I../src -o test_trie test_trie.c ../src/trie.c
* Run: ./test_trie /tmp/steno_test.bin
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "trie.h"
static uint8_t *load_file(const char *path, size_t *out_len)
{
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *buf = malloc(len);
if (fread(buf, 1, len, f) != (size_t)len) {
free(buf);
fclose(f);
return NULL;
}
fclose(f);
*out_len = len;
return buf;
}
static int tests_run = 0;
static int tests_passed = 0;
#define CHECK(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; } \
else { printf("FAIL: %s (line %d)\n", msg, __LINE__); } \
} while(0)
int main(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: %s <steno_dict.bin>\n", argv[0]);
return 1;
}
size_t len;
uint8_t *data = load_file(argv[1], &len);
if (!data) {
printf("Failed to load %s\n", argv[1]);
return 1;
}
int ret = steno_trie_init(data, len);
CHECK(ret == 0, "trie_init succeeds");
/* S → "is" (S- = bit 0 = 0x00000001) */
uint32_t s_stroke = 0x00000001;
const char *r = steno_trie_lookup(&s_stroke, 1);
CHECK(r != NULL && strcmp(r, "is") == 0, "S → 'is'");
/* T → "it" (T- = bit 1 = 0x00000002) */
uint32_t t_stroke = 0x00000002;
r = steno_trie_lookup(&t_stroke, 1);
CHECK(r != NULL && strcmp(r, "it") == 0, "T → 'it'");
/* -T → "the" (-T = bit 18 = 0x00040000) */
uint32_t t_right = 0x00040000;
r = steno_trie_lookup(&t_right, 1);
CHECK(r != NULL && strcmp(r, "the") == 0, "-T → 'the'");
/* TEFT → "test" (T- | -E | -F | -T = 0x02|0x400|0x1000|0x40000) */
uint32_t teft = 0x00041402;
r = steno_trie_lookup(&teft, 1);
CHECK(r != NULL && strcmp(r, "test") == 0, "TEFT → 'test'");
/* KO/PHAOURD → "computer" (multi-stroke) */
uint32_t ko = 0x00000104; /* K | O */
uint32_t phaourd = 0x001029A8; /* P | H | A | O | U | R | -D */
uint32_t multi[2] = {ko, phaourd};
r = steno_trie_lookup(multi, 2);
CHECK(r != NULL && strcmp(r, "computer") == 0, "KO/PHAOURD → 'computer'");
/* Non-existent stroke */
uint32_t nonsense = 0x003FFFFF;
r = steno_trie_lookup(&nonsense, 1);
CHECK(r == NULL, "nonsense stroke → NULL");
/* has_prefix: KO should be prefix of KO/PHAOURD */
CHECK(steno_trie_has_prefix(&ko, 1) == true, "KO is prefix");
/* has_prefix: S is NOT prefix of anything multi-stroke */
CHECK(steno_trie_has_prefix(&s_stroke, 1) == false, "S is not prefix");
/* NULL/zero args */
CHECK(steno_trie_lookup(NULL, 1) == NULL, "NULL strokes → NULL");
CHECK(steno_trie_lookup(&s_stroke, 0) == NULL, "0 count → NULL");
CHECK(steno_trie_has_prefix(NULL, 1) == false, "NULL prefix → false");
printf("\n%d/%d tests passed\n", tests_passed, tests_run);
free(data);
return tests_passed == tests_run ? 0 : 1;
}

200
tests/test_undo.c Normal file
View file

@ -0,0 +1,200 @@
/*
* Test undo ring buffer.
* Build: cc -I../src -o test_undo test_undo.c ../src/undo.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "undo.h"
static int tests_run = 0;
static int tests_passed = 0;
#define ASSERT(cond, msg) do { \
tests_run++; \
if (!(cond)) { \
printf("FAIL [%s:%d]: %s\n", __func__, __LINE__, msg); \
} else { \
tests_passed++; \
} \
} while (0)
/* 1. Init → count=0, pop returns NULL */
static void test_init(void)
{
struct stroke_history h;
steno_undo_init(&h);
ASSERT(steno_undo_count(&h) == 0, "count should be 0 after init");
ASSERT(steno_undo_pop(&h) == NULL, "pop on empty should return NULL");
ASSERT(steno_undo_peek(&h) == NULL, "peek on empty should return NULL");
}
/* 2. Push one → count=1, peek returns it */
static void test_push_one(void)
{
struct stroke_history h;
steno_undo_init(&h);
uint32_t strokes[] = {0xABCD};
steno_undo_push(&h, strokes, 1, 5, 1, 0x0F);
ASSERT(steno_undo_count(&h) == 1, "count should be 1");
const struct stroke_history_entry *e = steno_undo_peek(&h);
ASSERT(e != NULL, "peek should not be NULL");
ASSERT(e->strokes[0] == 0xABCD, "stroke data mismatch");
ASSERT(e->stroke_count == 1, "stroke_count mismatch");
ASSERT(e->output_len == 5, "output_len mismatch");
ASSERT(e->space_before == 1, "space_before mismatch");
ASSERT(e->fmt_flags == 0x0F, "fmt_flags mismatch");
}
/* 3. Push and pop → entry matches */
static void test_push_pop(void)
{
struct stroke_history h;
steno_undo_init(&h);
uint32_t strokes[] = {0x100, 0x200};
steno_undo_push(&h, strokes, 2, 7, 0, 0x03);
struct stroke_history_entry *e = steno_undo_pop(&h);
ASSERT(e != NULL, "pop should return entry");
ASSERT(e->strokes[0] == 0x100, "stroke[0] mismatch");
ASSERT(e->strokes[1] == 0x200, "stroke[1] mismatch");
ASSERT(e->stroke_count == 2, "stroke_count mismatch");
ASSERT(e->output_len == 7, "output_len mismatch");
ASSERT(e->space_before == 0, "space_before mismatch");
ASSERT(e->fmt_flags == 0x03, "fmt_flags mismatch");
ASSERT(steno_undo_count(&h) == 0, "count should be 0 after pop");
}
/* 4. Overflow: push SIZE+1 → count stays at SIZE */
static void test_overflow(void)
{
struct stroke_history h;
steno_undo_init(&h);
for (int i = 0; i < CONFIG_STENO_HISTORY_SIZE + 1; i++) {
uint32_t s = (uint32_t)i;
steno_undo_push(&h, &s, 1, (uint8_t)(i & 0xFF), 0, 0);
}
ASSERT(steno_undo_count(&h) == CONFIG_STENO_HISTORY_SIZE,
"count should cap at SIZE");
/* Most recent should be SIZE (last pushed) */
const struct stroke_history_entry *e = steno_undo_peek(&h);
ASSERT(e != NULL, "peek should not be NULL");
ASSERT(e->strokes[0] == (uint32_t)CONFIG_STENO_HISTORY_SIZE,
"most recent entry should be last pushed");
/* Oldest (entry 0) should be gone; entry 1 should be oldest */
/* Pop all and check last one is entry 1 */
struct stroke_history_entry *last = NULL;
for (int i = 0; i < CONFIG_STENO_HISTORY_SIZE; i++) {
last = steno_undo_pop(&h);
ASSERT(last != NULL, "pop should succeed");
}
/* last popped = oldest = entry index 1 */
ASSERT(last->strokes[0] == 1, "oldest entry should be index 1 (0 was overwritten)");
ASSERT(steno_undo_count(&h) == 0, "count should be 0 after popping all");
}
/* 5. Pop all → count=0 */
static void test_pop_all(void)
{
struct stroke_history h;
steno_undo_init(&h);
for (int i = 0; i < 10; i++) {
uint32_t s = (uint32_t)i;
steno_undo_push(&h, &s, 1, 1, 0, 0);
}
for (int i = 0; i < 10; i++) {
ASSERT(steno_undo_pop(&h) != NULL, "pop should succeed");
}
ASSERT(steno_undo_count(&h) == 0, "count should be 0");
ASSERT(steno_undo_pop(&h) == NULL, "pop on empty should be NULL");
}
/* 6. Push/pop cycle: push 5, pop 3, push 2, pop 4 → LIFO order */
static void test_push_pop_cycle(void)
{
struct stroke_history h;
steno_undo_init(&h);
/* Push 5: values 10,11,12,13,14 */
for (int i = 0; i < 5; i++) {
uint32_t s = (uint32_t)(10 + i);
steno_undo_push(&h, &s, 1, (uint8_t)(10 + i), 0, 0);
}
ASSERT(steno_undo_count(&h) == 5, "count should be 5");
/* Pop 3: should get 14, 13, 12 */
struct stroke_history_entry *e;
e = steno_undo_pop(&h);
ASSERT(e->strokes[0] == 14, "should pop 14");
e = steno_undo_pop(&h);
ASSERT(e->strokes[0] == 13, "should pop 13");
e = steno_undo_pop(&h);
ASSERT(e->strokes[0] == 12, "should pop 12");
ASSERT(steno_undo_count(&h) == 2, "count should be 2");
/* Push 2: values 20, 21 */
for (int i = 0; i < 2; i++) {
uint32_t s = (uint32_t)(20 + i);
steno_undo_push(&h, &s, 1, (uint8_t)(20 + i), 0, 0);
}
ASSERT(steno_undo_count(&h) == 4, "count should be 4");
/* Pop 4: should get 21, 20, 11, 10 */
e = steno_undo_pop(&h);
ASSERT(e->strokes[0] == 21, "should pop 21");
e = steno_undo_pop(&h);
ASSERT(e->strokes[0] == 20, "should pop 20");
e = steno_undo_pop(&h);
ASSERT(e->strokes[0] == 11, "should pop 11");
e = steno_undo_pop(&h);
ASSERT(e->strokes[0] == 10, "should pop 10");
ASSERT(steno_undo_count(&h) == 0, "count should be 0");
}
/* 7. Verify multi-stroke data preserved */
static void test_data_preservation(void)
{
struct stroke_history h;
steno_undo_init(&h);
uint32_t strokes[] = {0xDEAD, 0xBEEF, 0xCAFE};
steno_undo_push(&h, strokes, 3, 42, 1, 0x7A);
struct stroke_history_entry *e = steno_undo_pop(&h);
ASSERT(e != NULL, "pop should return entry");
ASSERT(e->stroke_count == 3, "stroke_count should be 3");
ASSERT(e->strokes[0] == 0xDEAD, "strokes[0] mismatch");
ASSERT(e->strokes[1] == 0xBEEF, "strokes[1] mismatch");
ASSERT(e->strokes[2] == 0xCAFE, "strokes[2] mismatch");
ASSERT(e->output_len == 42, "output_len mismatch");
ASSERT(e->space_before == 1, "space_before mismatch");
ASSERT(e->fmt_flags == 0x7A, "fmt_flags mismatch");
}
int main(void)
{
printf("Running undo tests...\n\n");
test_init();
test_push_one();
test_push_pop();
test_overflow();
test_pop_all();
test_push_pop_cycle();
test_data_preservation();
printf("\n%d / %d tests passed\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}