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:
parent
234bc8d731
commit
1ec204b845
33 changed files with 5443 additions and 0 deletions
64
tests/run_tests.sh
Executable file
64
tests/run_tests.sh
Executable 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
340
tests/test_formatter.c
Normal 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
383
tests/test_mphf.c
Normal 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
102
tests/test_trie.c
Normal 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
200
tests/test_undo.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue