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

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;
}