/** * 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 #include #include #include #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 \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; }