From 42823420ab3d2f35109e4f7fbe696b9b1070c121 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 11 Aug 2026 19:35:11 +0900 Subject: [PATCH] add focused unit and map checks --- .gitmodules | 3 + Makefile | 25 ++- README.md | 24 ++- cutest | 1 + tests/Makefile | 34 ++++ tests/data/hira.map | 2 + tests/data/trie.map | 6 + tests/dict_test.c | 53 ++++++ tests/engine_test.c | 158 +++++++++++++++++ tests/hash_test.c | 78 +++++++++ tests/ipc_test.c | 411 ++++++++++++++++++++++++++++++++++++++++++++ tests/ko_test.c | 60 +++++++ tests/str_test.c | 84 +++++++++ tests/test.h | 42 +++++ tests/test_util.c | 32 ++++ tests/trie_test.c | 126 ++++++++++++++ tests/unit_test.c | 112 ++++++++++++ tests/vi_test.c | 84 +++++++++ 18 files changed, 1326 insertions(+), 9 deletions(-) create mode 100644 .gitmodules create mode 160000 cutest create mode 100644 tests/Makefile create mode 100644 tests/data/hira.map create mode 100644 tests/data/trie.map create mode 100644 tests/dict_test.c create mode 100644 tests/engine_test.c create mode 100644 tests/hash_test.c create mode 100644 tests/ipc_test.c create mode 100644 tests/ko_test.c create mode 100644 tests/str_test.c create mode 100644 tests/test.h create mode 100644 tests/test_util.c create mode 100644 tests/trie_test.c create mode 100644 tests/unit_test.c create mode 100644 tests/vi_test.c diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0d09528 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cutest"] + path = cutest + url = ../cutest.git diff --git a/Makefile b/Makefile index 8ed75fa..e8d9477 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,15 @@ CC = 9c LD = 9l +PKGGOALS := $(filter-out check test verify-map clean xim bench,$(MAKECMDGOALS)) +ifeq ($(MAKECMDGOALS),) +PKGGOALS := all +endif +ifneq ($(PKGGOALS),) DBUS_CFLAGS := $(shell pkg-config --cflags dbus-1) DBUS_LIBS := $(shell pkg-config --libs dbus-1) WL_CFLAGS := $(shell pkg-config --cflags wayland-client xkbcommon) WL_LIBS := $(shell pkg-config --libs wayland-client xkbcommon) +endif CFLAGS = -Wall -Wextra -O2 -g $(DBUS_CFLAGS) $(WL_CFLAGS) PROG = strans @@ -19,13 +25,22 @@ $(OBJS): dat.h fn.h ipc.h clean: rm -f $(OBJS) $(PROG) - make -C xim/ clean - make -C bench/ clean + $(MAKE) -C tests clean + $(MAKE) -C xim/ clean + $(MAKE) -C bench/ clean xim: - make -C xim/ + $(MAKE) -C xim/ bench: - make -C bench/ + $(MAKE) -C bench/ -.PHONY: all clean xim bench +check: verify-map test + +test: + $(MAKE) -C tests check TESTARGS="$(TESTARGS)" + +verify-map: + python3 map/mktelex.py | cmp - map/telex.map + +.PHONY: all check test verify-map clean xim bench diff --git a/README.md b/README.md index c0bbe25..4e17cc5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Inspired by 9front's ktrans. Threads communicate via CSP channels. ## Dependencies - plan9port +- Python 3 (for generated-map checks) - dbus-1 - wayland-client, libxkbcommon (for Wayland support) - gtk+-3.0 (optional, for GTK IM module) @@ -17,6 +18,21 @@ Inspired by 9front's ktrans. Threads communicate via CSP channels. cd xim && make # XIM adapter cd gtk && make docker && make install # GTK IM module +Unit tests use the sibling `cutest` Git repository as a submodule and do not +require the Wayland, D-Bus, GTK, or X11 development packages. Repository +mirrors must provide the same sibling; initialize it after cloning with +`git submodule update --init`: + + make check # generated map + unit tests + make test TESTARGS=hangul # filtered unit tests only + make verify-map # generated-map check only + +The suite covers the fixed-size UTF-8 string, hash table, trie fixtures, +Japanese/Hangul/Telex state transitions, dictionary candidates, engine +selection state, and the byte-stream IPC path. Tests use explicit case tables, +boundary values, and small regressions for repaired core contracts. GUI +protocol stacks are outside this suite and require separate integration checks. + ## Run ./strans map font & @@ -60,10 +76,10 @@ Tab or Enter to commit. Threads communicate via CSP channels: -- [imthread](strans.c#L271): keystroke processing, transliteration -- [dictthread](dict.c#L38): dictionary lookup -- [drawthread](win.c#L133): preedit window rendering -- [srvthread](srv.c#L42): IPC via unix socket +- [imthread](strans.c): keystroke processing, transliteration +- [dictthread](dict.c): dictionary lookup +- [drawthread](win.c): preedit window rendering +- [srvthread](srv.c): IPC via unix socket - [ibusthread](ibus.c): IBus D-Bus endpoint for GLFW/kitty - [waylandthread](wayland.c): Wayland input-method-v2 + virtual-keyboard-v1 diff --git a/cutest b/cutest new file mode 160000 index 0000000..7894ba7 --- /dev/null +++ b/cutest @@ -0,0 +1 @@ +Subproject commit 7894ba7a26eb97a7ba06931e2c4e3e7528188320 diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..89ce884 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,34 @@ +CC = 9c +LD = 9l +CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest +LIBS = -lthread -lbio + +PROG = unit_test +TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ + ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c +TESTOBJ = $(TESTSRC:.c=.o) +PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c srv.c +PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) +OBJS = $(TESTOBJ) $(PARENTOBJ) + +all: $(PROG) + +check test: $(PROG) + ./$(PROG) $(TESTARGS) + +$(PROG): $(OBJS) + $(LD) -o $@ $(OBJS) $(LIBS) + +$(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h ../cutest/cutest.h +engine_test.o: ../strans.c + +unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h + $(CC) $(CFLAGS) -c -o $@ $< + +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +clean: + rm -f $(OBJS) $(PROG) + +.PHONY: all check test clean diff --git a/tests/data/hira.map b/tests/data/hira.map new file mode 100644 index 0000000..5c324a9 --- /dev/null +++ b/tests/data/hira.map @@ -0,0 +1,2 @@ +ka か +sa さ diff --git a/tests/data/trie.map b/tests/data/trie.map new file mode 100644 index 0000000..1275077 --- /dev/null +++ b/tests/data/trie.map @@ -0,0 +1,6 @@ +a alpha +ab beta +한 값 +duplicate first +duplicate second +tabs one two diff --git a/tests/dict_test.c b/tests/dict_test.c new file mode 100644 index 0000000..93ae638 --- /dev/null +++ b/tests/dict_test.c @@ -0,0 +1,53 @@ +#include "test.h" + +void +dictionary_candidates(struct ct *t) +{ + char many[512], item[8]; + char *p; + Dictreq req; + Dictres res; + Hmap *saved; + Lang *lang; + Str key; + int i; + + lang = getlang(LangJP); + saved = lang->dict; + lang->dict = hmapalloc(1); + key = mkstr("かな"); + hmapset(&lang->dict, &key, " 候補1 かな 候補2 ", + strlen(" 候補1 かな 候補2 ")); + memset(&req, 0, sizeof req); + req.key = key; + req.pre = mkstr("preedit-one"); + req.lang = LangJP; + dictlookup(&req, &res); + if(!CT_EQ_INT(t, 2, res.nkouho)) + goto cleanup; + CT_EQ_INT(t, 0, scmp(&req.pre, &res.key)); + checkstr(t, "candidate 1", "候補1", &res.kouho[0]); + checkstr(t, "candidate 2", "候補2", &res.kouho[1]); + key = mkstr("key"); + p = many; + for(i = 0; i < Maxkouho+1; i++){ + snprint(item, sizeof item, "c%02d", i); + if(i > 0) + *p++ = ' '; + memmove(p, item, strlen(item)); + p += strlen(item); + } + *p = '\0'; + hmapset(&lang->dict, &key, many, strlen(many)); + req.key = key; + req.pre = mkstr("preedit-two"); + dictlookup(&req, &res); + if(!CT_EQ_INT(t, Maxkouho, res.nkouho)) + goto cleanup; + CT_EQ_INT(t, 0, scmp(&req.pre, &res.key)); + checkstr(t, "first capped candidate", "c00", &res.kouho[0]); + checkstr(t, "last capped candidate", "c31", &res.kouho[31]); +cleanup: + hmapfree(lang->dict); + lang->dict = saved; +} diff --git a/tests/engine_test.c b/tests/engine_test.c new file mode 100644 index 0000000..eb89373 --- /dev/null +++ b/tests/engine_test.c @@ -0,0 +1,158 @@ +#define STRANS_TEST_ENGINE +#include "../strans.c" +#include "test.h" + +void +vietnamese_state_lifetime(struct ct *t) +{ + Str com; + + init(); + im.l = &testvi; + im.pre = mkstr("as"); + im.raw = mkstr("as"); + sclear(&com); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "committed Telex", "á", &com); + CT_EQ_INT(t, 0, im.pre.n); + CT_EQ_INT(t, 0, im.raw.n); + + init(); + im.l = &testvi; + im.pre = mkstr("as"); + im.raw = mkstr("as"); + sclear(&com); + CT_CHECK(t, keystroke(Kesc, 0, &com)); + CT_EQ_INT(t, 0, com.n); + CT_EQ_INT(t, 0, im.pre.n); + CT_EQ_INT(t, 0, im.raw.n); + + init(); + im.l = &testvi; + im.pre = mkstr("as"); + im.raw = mkstr("as"); + sclear(&com); + CT_CHECK(t, dotrans('q', &com)); + checkstr(t, "completed Telex", "á", &com); + checkstr(t, "next preedit", "q", &im.pre); + checkstr(t, "next raw input", "q", &im.raw); +} + +void +engine_clears_candidates(struct ct *t) +{ + Str com; + + init(); + im.l = getlang(LangKO); + im.pre = mkstr("ㄱ"); + im.nkouho = 2; + im.sel = 1; + sclear(&com); + dotrans('k', &com); + CT_EQ_INT(t, 0, im.nkouho); + CT_EQ_INT(t, -1, im.sel); +} + +void +engine_selects_visible_candidate(struct ct *t) +{ + static const struct { int n, sel; Rune key; char *want; } cases[] = { + { Maxdisp, -1, '9', "c9" }, + { Maxdisp+3, Maxdisp+1, '1', "c3" }, + }; + char name[8]; + Str com; + int i, j; + + for(i = 0; i < nelem(cases); i++){ + init(); + for(j = 0; j < cases[i].n; j++){ + snprint(name, sizeof name, "c%d", j+1); + im.kouho[j] = mkstr(name); + } + im.nkouho = cases[i].n; + im.sel = cases[i].sel; + sclear(&com); + CT_CHECK(t, keystroke(cases[i].key, 0, &com)); + checkstr(t, cases[i].want, cases[i].want, &com); + CT_EQ_INT(t, 0, im.nkouho); + } +} + +void +engine_commit_contract(struct ct *t) +{ + Str com; + + init(); + im.l = getlang(LangJP); + im.pre = mkstr("ka"); + im.nkouho = 1; + im.sel = -1; + sclear(&com); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "return commit", "か", &com); + CT_EQ_INT(t, 0, im.pre.n); + CT_EQ_INT(t, 0, im.raw.n); + CT_EQ_INT(t, 0, im.nkouho); + CT_EQ_INT(t, -1, im.sel); + + init(); + im.l = getlang(LangJP); + im.pre = mkstr("ka"); + im.kouho[0] = mkstr("c1"); + im.kouho[1] = mkstr("c2"); + im.nkouho = 2; + im.sel = 1; + sclear(&com); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "selected candidate", "c2", &com); + CT_EQ_INT(t, 0, im.pre.n); + CT_EQ_INT(t, 0, im.nkouho); + CT_EQ_INT(t, -1, im.sel); + + init(); + im.l = getlang(LangJP); + im.pre = mkstr("ka"); + im.nkouho = 1; + im.sel = -1; + sclear(&com); + CT_CHECK(t, !keystroke('q', 0, &com)); + checkstr(t, "uneaten-key commit", "か", &com); + CT_EQ_INT(t, 0, im.pre.n); + CT_EQ_INT(t, 0, im.raw.n); + CT_EQ_INT(t, 0, im.nkouho); + CT_EQ_INT(t, -1, im.sel); + + init(); + im.l = getlang(LangJP); + im.pre = mkstr("ka"); + sclear(&com); + CT_CHECK(t, keystroke(0xf008, 0, &com)); + CT_EQ_INT(t, 2, com.n); + CT_EQ_UINT(t, 0x304b, com.r[0]); + CT_EQ_UINT(t, 0xf008, com.r[1]); + CT_EQ_INT(t, 0, im.pre.n); +} + +void +engine_telex_history_bound(struct ct *t) +{ + Str com; + Rune tone; + int i; + + init(); + im.l = &testvi; + sclear(&com); + CT_CHECK(t, keystroke('a', 0, &com)); + for(i = 0; i < Maxrunes; i++){ + tone = i % 2 == 0 ? 's' : 'f'; + if(!CT_CHECK(t, keystroke(tone, 0, &com))) + break; + } + checkstr(t, "bounded Telex commit", "à", &com); + CT_EQ_INT(t, 0, im.pre.n); + CT_EQ_INT(t, 0, im.raw.n); +} diff --git a/tests/hash_test.c b/tests/hash_test.c new file mode 100644 index 0000000..631809d --- /dev/null +++ b/tests/hash_test.c @@ -0,0 +1,78 @@ +#include "test.h" + +void +hmap_set_replace_and_grow(struct ct *t) +{ + char keybuf[16], valbuf[16], source[] = "copied"; + Hmap *h; + Hnode *n; + Str key; + int i; + + h = hmapalloc(1); + for(i = 0; i < 4; i++){ + snprint(keybuf, sizeof keybuf, "key%d", i); + snprint(valbuf, sizeof valbuf, "value%d", i); + key = mkstr(keybuf); + hmapset(&h, &key, valbuf, strlen(valbuf)); + } + for(i = 0; i < 4; i++){ + snprint(keybuf, sizeof keybuf, "key%d", i); + snprint(valbuf, sizeof valbuf, "value%d", i); + key = mkstr(keybuf); + n = hmapget(h, &key); + if(n == nil || strcmp(n->val, valbuf) != 0) + CT_ERRORF(t, "%s: collision chain lost value", keybuf); + } + key = mkstr("key1"); + hmapset(&h, &key, source, strlen(source)); + source[0] = 'X'; + n = hmapget(h, &key); + if(!CT_CHECK(t, n != nil)) + goto cleanup; + CT_EQ_STR(t, "copied", n->val); + key = mkstr("key2"); + n = hmapget(h, &key); + if(!CT_CHECK(t, n != nil)) + goto cleanup; + CT_EQ_STR(t, "value2", n->val); + key = mkstr("key1"); + hmapset(&h, &key, nil, 0); + n = hmapget(h, &key); + if(!CT_CHECK(t, n != nil)) + goto cleanup; + CT_EQ_INT(t, 0, n->vlen); + CT_EQ_PTR(t, nil, n->val); + key = mkstr("missing"); + CT_EQ_PTR(t, nil, hmapget(h, &key)); +cleanup: + hmapfree(h); +} + +void +hmap_long_utf8_keys(struct ct *t) +{ + Hmap *h; + Hnode *n; + Str a, b; + int i; + + for(i = 0; i < Maxrunes-1; i++) + a.r[i] = b.r[i] = 0x1f600; + a.r[Maxrunes-1] = 0x1f601; + b.r[Maxrunes-1] = 0x1f602; + a.n = b.n = Maxrunes; + h = hmapalloc(1); + hmapset(&h, &a, "first", 5); + hmapset(&h, &b, "second", 6); + n = hmapget(h, &a); + if(!CT_CHECK(t, n != nil)) + goto cleanup; + CT_EQ_STR(t, "first", n != nil ? n->val : nil); + n = hmapget(h, &b); + if(!CT_CHECK(t, n != nil)) + goto cleanup; + CT_EQ_STR(t, "second", n != nil ? n->val : nil); +cleanup: + hmapfree(h); +} diff --git a/tests/ipc_test.c b/tests/ipc_test.c new file mode 100644 index 0000000..7854076 --- /dev/null +++ b/tests/ipc_test.c @@ -0,0 +1,411 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test.h" + +#undef waitpid + +enum +{ + Childtimeout = 2000, +}; + +typedef struct Childres Childres; +struct Childres +{ + int rc; + unsigned int want; + unsigned int mod; + unsigned int key; +}; + +static long long +nowms(void) +{ + struct timespec ts; + + if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0) + return -1; + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int +waitreadable(int fd, int timeout) +{ + struct pollfd pfd; + long long end, now; + int n, left; + + end = nowms() + timeout; + pfd.fd = fd; + pfd.events = POLLIN; + for(;;){ + now = nowms(); + if(now < 0 || now >= end) + return -1; + left = end - now; + n = poll(&pfd, 1, left); + if(n > 0) + return (pfd.revents & (POLLIN|POLLHUP)) != 0 ? 0 : -1; + if(n == 0) + return -1; + if(errno != EINTR) + return -1; + } +} + +static int +readdeadline(int fd, void *buf, size_t n, int timeout) +{ + unsigned char *p; + long long end, now; + ssize_t got; + + p = buf; + end = nowms() + timeout; + while(n > 0){ + now = nowms(); + if(now < 0 || now >= end || waitreadable(fd, end - now) < 0) + return -1; + got = read(fd, p, n); + if(got < 0 && errno == EINTR) + continue; + if(got <= 0) + return -1; + p += got; + n -= got; + } + return 0; +} + +static int +waitqueueempty(int fd, int timeout) +{ + long long end; + int n; + + end = nowms() + timeout; + for(;;){ + if(ioctl(fd, FIONREAD, &n) < 0) + return -1; + if(n == 0) + return 0; + if(nowms() >= end) + return -1; + sched_yield(); + } +} + +static int +reapchild(pid_t pid, int *status, int timeout) +{ + long long end; + pid_t got; + + end = nowms() + timeout; + for(;;){ + got = waitpid(pid, status, WNOHANG); + if(got == pid) + return 0; + if(got < 0 && errno != EINTR) + return -1; + if(nowms() >= end) + break; + poll(nil, 0, 1); + } + if(kill(pid, SIGKILL) < 0 && errno != ESRCH) + return -1; + end = nowms() + timeout; + for(;;){ + got = waitpid(pid, status, WNOHANG); + if(got == pid) + return 0; + if(got < 0 && errno != EINTR) + return -1; + if(nowms() >= end) + return -1; + poll(nil, 0, 1); + } +} + +void +ipc_request_codec(struct ct *t) +{ + static const struct { + int want; + unsigned int mod, key; + unsigned char bytes[Ipcreqsz]; + } cases[] = { + { 0, 0, 0, { 0, 0, 0, 0, 0, 0 } }, + { 1, Mctrl|Mshift, 0x1234, + { 1, Mctrl|Mshift, 0x34, 0x12, 0, 0 } }, + { 1, Malt, 0x1f600, + { 1, Malt, 0x00, 0xf6, 0x01, 0x00 } }, + { 1, 0, 0xf008, + { 1, 0, 0x08, 0xf0, 0x00, 0x00 } }, + { 1, 0, Kback, + { 1, 0, 0x08, 0x00, 0x11, 0x00 } }, + { 1, Msuper, 0x89abcdefU, + { 1, Msuper, 0xef, 0xcd, 0xab, 0x89 } }, + }; + unsigned char got[Ipcreqsz]; + unsigned int mod, key; + int i, want; + + for(i = 0; i < nelem(cases); i++){ + ipcpackreq(got, cases[i].want, cases[i].mod, cases[i].key); + if(!CT_EQ_MEM(t, cases[i].bytes, got, sizeof got)) + continue; + ipcunpackreq(got, &want, &mod, &key); + CT_EQ_INT(t, cases[i].want != 0, want); + CT_EQ_UINT(t, cases[i].mod, mod); + CT_EQ_UINT(t, cases[i].key, key); + } +} + +void +ipc_response_pack_boundaries(struct ct *t) +{ + static const unsigned char want[] = { 1, 1, 0, 'A', 2, 0, 'x', 'y' }; + static const unsigned char want0[] = { 1, 1, 0, 'A' }; + unsigned char out[Ipcmaxresp+1], field[Ipcfieldmax]; + int n; + + n = ipcpackresp(out, sizeof out, 7, "A", 1, "xy", 2, 1); + CT_EQ_INT(t, sizeof want, n); + CT_EQ_MEM(t, want, out, sizeof want); + n = ipcpackresp(out, sizeof out, 1, "A", 1, "xy", 2, 0); + CT_EQ_INT(t, sizeof want0, n); + CT_EQ_MEM(t, want0, out, sizeof want0); + + memset(field, 'x', sizeof field); + memset(out, 0xa5, sizeof out); + n = ipcpackresp(out, Ipcmaxresp, 1, + (char*)field, sizeof field, (char*)field, sizeof field, 1); + CT_EQ_INT(t, Ipcmaxresp, n); + CT_EQ_INT(t, 0, out[1]); + CT_EQ_INT(t, 1, out[2]); + CT_EQ_INT(t, 0, out[3+Ipcfieldmax]); + CT_EQ_INT(t, 1, out[4+Ipcfieldmax]); + CT_EQ_INT(t, 0xa5, out[Ipcmaxresp]); + CT_EQ_INT(t, 'x', out[Ipcmaxresp-1]); + + memset(out, 0xa5, sizeof out); + CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1, + (char*)field, sizeof field, (char*)field, sizeof field, 1)); + CT_EQ_INT(t, 0xa5, out[0]); + CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, + nil, 1, nil, 0, 0)); + CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, + nil, 0, nil, 1, 1)); + CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, + (char*)field, Ipcfieldmax+1, nil, 0, 0)); +} + +void +ipc_response_codec_and_drain(struct ct *t) +{ + unsigned char first[Ipcmaxresp]; + static const unsigned char second[] = { 0, 2, 0, 'o', 'k' }; + unsigned char field[Ipcfieldmax]; + char commit[8], preedit[8]; + Ipcresp resp; + int fd[2], n; + + memset(field, 'x', sizeof field); + n = ipcpackresp(first, sizeof first, 1, + (char*)field, sizeof field, (char*)field, sizeof field, 1); + if(!CT_EQ_INT(t, Ipcmaxresp, n)) + return; + if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){ + CT_ERRORF(t, "socketpair failed"); + return; + } + if(!CT_EQ_INT(t, 0, ipcsend(fd[0], first, n))) + goto cleanup; + if(!CT_EQ_INT(t, 0, ipcsend(fd[0], second, sizeof second))) + goto cleanup; + if(!CT_EQ_INT(t, 0, shutdown(fd[0], SHUT_WR))) + goto cleanup; + if(!CT_EQ_INT(t, 0, + ipcreadresp(fd[1], 1, commit, sizeof commit, + preedit, sizeof preedit, &resp))) + goto cleanup; + CT_EQ_INT(t, 1, resp.eaten); + CT_EQ_SIZE(t, Ipcfieldmax, resp.ncommit); + CT_EQ_SIZE(t, Ipcfieldmax, resp.npreedit); + CT_EQ_STR(t, "xxxxxxx", commit); + CT_EQ_STR(t, "xxxxxxx", preedit); + if(!CT_EQ_INT(t, 0, + ipcreadresp(fd[1], 0, commit, sizeof commit, + preedit, sizeof preedit, &resp))) + goto cleanup; + CT_EQ_INT(t, 0, resp.eaten); + CT_EQ_STR(t, "ok", commit); + CT_EQ_STR(t, "", preedit); +cleanup: + close(fd[0]); + close(fd[1]); +} + +void +ipc_truncated_frame(struct ct *t) +{ + unsigned char b = 1, req[Ipcreqsz]; + int fd[2]; + + if(pipe(fd) < 0){ + CT_ERRORF(t, "pipe failed"); + return; + } + if(!CT_EQ_INT(t, 1, write(fd[1], &b, 1))){ + close(fd[0]); + close(fd[1]); + return; + } + close(fd[1]); + CT_EQ_INT(t, -1, ipcreadn(fd[0], req, sizeof req)); + close(fd[0]); +} + +void +ipc_fragmented_request(struct ct *t) +{ + unsigned char req[Ipcreqsz]; + Childres res; + Keyreq got; + pid_t pid; + int data[2], result[2], status; + + pid = -1; + data[0] = data[1] = result[0] = result[1] = -1; + ipcpackreq(req, 1, Mctrl|Mshift, 0x1f600); + if(socketpair(AF_UNIX, SOCK_STREAM, 0, data) < 0){ + CT_ERRORF(t, "data socketpair failed"); + return; + } + if(socketpair(AF_UNIX, SOCK_STREAM, 0, result) < 0){ + CT_ERRORF(t, "result socketpair failed"); + goto cleanup; + } + pid = fork(); + if(pid < 0){ + CT_ERRORF(t, "fork failed"); + goto cleanup; + } + if(pid == 0){ + close(data[0]); + close(result[0]); + memset(&res, 0, sizeof res); + res.rc = srvreadkey(data[1], &got); + if(res.rc == 0){ + res.want = got.want; + res.mod = got.mod; + res.key = got.ks; + } + _exit(ipcsend(result[1], &res, sizeof res) < 0 ? 1 : 0); + } + close(result[1]); + result[1] = -1; + if(ipcsend(data[0], req, Ipcreqsz/2) < 0){ + CT_ERRORF(t, "first request fragment failed"); + goto cleanup; + } + if(waitqueueempty(data[1], Childtimeout) < 0){ + CT_ERRORF(t, "child did not consume first request fragment"); + goto cleanup; + } + if(ipcsend(data[0], req+Ipcreqsz/2, Ipcreqsz-Ipcreqsz/2) < 0){ + CT_ERRORF(t, "second request fragment failed"); + goto cleanup; + } + if(readdeadline(result[0], &res, sizeof res, Childtimeout) < 0){ + CT_ERRORF(t, "timed out waiting for decoded request"); + goto cleanup; + } + CT_EQ_INT(t, 0, res.rc); + CT_EQ_INT(t, 1, res.want); + CT_EQ_UINT(t, Mctrl|Mshift, res.mod); + CT_EQ_UINT(t, 0x1f600, res.key); +cleanup: + if(data[0] >= 0) + close(data[0]); + if(data[1] >= 0) + close(data[1]); + if(result[0] >= 0) + close(result[0]); + if(result[1] >= 0) + close(result[1]); + if(pid > 0){ + if(reapchild(pid, &status, Childtimeout) < 0) + CT_ERRORF(t, "could not reap request reader"); + else if(!WIFEXITED(status) || WEXITSTATUS(status) != 0) + CT_ERRORF(t, "request reader failed"); + } +} + +void +ipc_closed_peer(struct ct *t) +{ + unsigned char b; + pid_t pid; + int data[2], syncfd[2], status; + + pid = -1; + data[0] = data[1] = syncfd[0] = syncfd[1] = -1; + if(socketpair(AF_UNIX, SOCK_STREAM, 0, data) < 0){ + CT_ERRORF(t, "data socketpair failed"); + return; + } + if(socketpair(AF_UNIX, SOCK_STREAM, 0, syncfd) < 0){ + CT_ERRORF(t, "synchronization socketpair failed"); + goto cleanup; + } + pid = fork(); + if(pid < 0){ + CT_ERRORF(t, "fork failed"); + goto cleanup; + } + if(pid == 0){ + signal(SIGPIPE, SIG_DFL); + close(data[1]); + close(syncfd[0]); + if(ipcsend(syncfd[1], "r", 1) < 0 || + readdeadline(syncfd[1], &b, 1, Childtimeout) < 0) + _exit(2); + _exit(ipcsend(data[0], "x", 1) < 0 ? 0 : 1); + } + close(data[0]); + data[0] = -1; + close(data[1]); + data[1] = -1; + close(syncfd[1]); + syncfd[1] = -1; + if(readdeadline(syncfd[0], &b, 1, Childtimeout) < 0 || + ipcsend(syncfd[0], "g", 1) < 0) + CT_ERRORF(t, "child synchronization failed"); +cleanup: + if(data[0] >= 0) + close(data[0]); + if(data[1] >= 0) + close(data[1]); + if(syncfd[0] >= 0) + close(syncfd[0]); + if(syncfd[1] >= 0) + close(syncfd[1]); + if(pid > 0){ + if(reapchild(pid, &status, Childtimeout) < 0) + CT_ERRORF(t, "could not reap closed-peer writer"); + else if(!WIFEXITED(status) || WEXITSTATUS(status) != 0) + CT_ERRORF(t, "write to closed peer did not return -1"); + } +} diff --git a/tests/ko_test.c b/tests/ko_test.c new file mode 100644 index 0000000..405d60a --- /dev/null +++ b/tests/ko_test.c @@ -0,0 +1,60 @@ +#include "test.h" + +void +korean_sequences(struct ct *t) +{ + static const struct { char *keys, *want; } cases[] = { + { "r", "ㄱ" }, + { "rk", "가" }, + { "rkr", "각" }, + { "rkrk", "가가" }, + { "rkrtk", "각사" }, + { "rhk", "과" }, + { "rr", "ㄱㄱ" }, + { "Rk", "까" }, + { "rk1", "가1" }, + }; + Emit e; + Im state; + Str out; + Rune key; + char *p; + int i; + + for(i = 0; i < nelem(cases); i++){ + memset(&state, 0, sizeof state); + state.l = getlang(LangKO); + sclear(&out); + for(p = cases[i].keys; *p != '\0'; p++){ + key = (uchar)*p; + e = transko(&state, key); + sappend(&out, &e.s); + state.pre = e.next; + if(!e.eat) + sputr(&out, key); + } + sappend(&out, &state.pre); + checkstr(t, cases[i].keys, cases[i].want, &out); + } +} + +void +korean_backspace(struct ct *t) +{ + static const struct { char *before, *after; } cases[] = { + { "ㄱ", "" }, + { "가", "ㄱ" }, + { "과", "고" }, + { "각", "가" }, + { "갃", "각" }, + }; + Im state; + int i; + + memset(&state, 0, sizeof state); + for(i = 0; i < nelem(cases); i++){ + state.pre = mkstr(cases[i].before); + backko(&state); + checkstr(t, cases[i].before, cases[i].after, &state.pre); + } +} diff --git a/tests/str_test.c b/tests/str_test.c new file mode 100644 index 0000000..4f1ed81 --- /dev/null +++ b/tests/str_test.c @@ -0,0 +1,84 @@ +#include "test.h" + +void +str_init_utf8(struct ct *t) +{ + char full[Maxrunes+2]; + char incomplete[] = { (char)0xea, (char)0xb0 }; + Str s; + + s = mkstr("A한😀"); + CT_EQ_INT(t, 3, s.n); + checkstr(t, "round trip", "A한😀", &s); + sinit(&s, incomplete, sizeof incomplete); + CT_EQ_INT(t, 0, s.n); + memset(full, 'a', sizeof full); + full[sizeof full-1] = '\0'; + sinit(&s, full, strlen(full)); + CT_EQ_INT(t, Maxrunes, s.n); +} + +void +str_edit_and_alias(struct ct *t) +{ + Str s; + int i; + + s = mkstr("ab"); + sappend(&s, &s); + checkstr(t, "self append", "abab", &s); + for(i = 0; i < 40; i++) + s.r[i] = 'a' + i % 26; + s.n = 40; + sappend(&s, &s); + CT_EQ_INT(t, Maxrunes, s.n); + for(i = 0; i < 24; i++) + if(s.r[40+i] != s.r[i]) + CT_ERRORF(t, "capped self append differs at rune %d", i); +} + +void +str_utf8_capacity(struct ct *t) +{ + static const struct { + int size; + int n; + char *want; + } cases[] = { + { 0, 0, nil }, + { 1, 0, "" }, + { 2, 1, "a" }, + { 3, 1, "a" }, + { 4, 1, "a" }, + { 5, 4, "a한" }, + { 6, 5, "a한b" }, + }; + char buf[16]; + Str s; + int i, n; + + s = mkstr("a한b"); + for(i = 0; i < nelem(cases); i++){ + memset(buf, 'Z', sizeof buf); + n = stoutf(&s, buf, cases[i].size); + if(n != cases[i].n) + CT_ERRORF(t, "size %d: want length %d, got %d", + cases[i].size, cases[i].n, n); + if(cases[i].size == 0){ + CT_EQ_INT(t, 'Z', buf[0]); + continue; + } + if(strcmp(cases[i].want, buf) != 0) + CT_ERRORF(t, "size %d: want \"%s\", got \"%s\"", + cases[i].size, cases[i].want, buf); + CT_EQ_INT(t, 'Z', buf[cases[i].size]); + } + + s = mkstr("😀"); + memset(buf, 'Z', sizeof buf); + CT_EQ_INT(t, 0, stoutf(&s, buf, 4)); + CT_EQ_STR(t, "", buf); + CT_EQ_INT(t, 4, stoutf(&s, buf, 5)); + CT_EQ_STR(t, "😀", buf); + CT_EQ_INT(t, 'Z', buf[5]); +} diff --git a/tests/test.h b/tests/test.h new file mode 100644 index 0000000..e1084ec --- /dev/null +++ b/tests/test.h @@ -0,0 +1,42 @@ +#ifndef STRANS_TEST_H +#define STRANS_TEST_H + +#include "../cutest/cutest.h" + +#ifndef STRANS_TEST_ENGINE +#include "../dat.h" +#include "../fn.h" +#endif + +extern Lang testvi; + +Str mkstr(char*); +int checkstr(struct ct*, char*, char*, Str*); +Str shownpre(Im*); + +void str_init_utf8(struct ct*); +void str_edit_and_alias(struct ct*); +void str_utf8_capacity(struct ct*); +void hmap_set_replace_and_grow(struct ct*); +void hmap_long_utf8_keys(struct ct*); +void trie_exact_prefix_and_duplicate(struct ct*); +void production_maps_load(struct ct*); +void transmap_states(struct ct*); +void korean_sequences(struct ct*); +void korean_backspace(struct ct*); +void vietnamese_transitions(struct ct*); +void vietnamese_backspace(struct ct*); +void vietnamese_state_lifetime(struct ct*); +void engine_clears_candidates(struct ct*); +void engine_selects_visible_candidate(struct ct*); +void engine_commit_contract(struct ct*); +void engine_telex_history_bound(struct ct*); +void dictionary_candidates(struct ct*); +void ipc_request_codec(struct ct*); +void ipc_response_pack_boundaries(struct ct*); +void ipc_response_codec_and_drain(struct ct*); +void ipc_truncated_frame(struct ct*); +void ipc_fragmented_request(struct ct*); +void ipc_closed_peer(struct ct*); + +#endif diff --git a/tests/test_util.c b/tests/test_util.c new file mode 100644 index 0000000..f7364d8 --- /dev/null +++ b/tests/test_util.c @@ -0,0 +1,32 @@ +#include "test.h" + +Str +mkstr(char *s) +{ + Str r; + + sinit(&r, s, strlen(s)); + return r; +} + +int +checkstr(struct ct *t, char *where, char *want, Str *got) +{ + char buf[Maxutf]; + + stoutf(got, buf, sizeof buf); + if(strcmp(want, buf) == 0) + return 1; + return CT_ERRORF(t, "%s: want \"%s\", got \"%s\"", + where, want, buf); +} + +Str +shownpre(Im *state) +{ + Str shown; + + if(state->l->map != nil && mapget(state->l->map, &state->pre, &shown)) + return shown; + return state->pre; +} diff --git a/tests/trie_test.c b/tests/trie_test.c new file mode 100644 index 0000000..b3f85b1 --- /dev/null +++ b/tests/trie_test.c @@ -0,0 +1,126 @@ +#include "test.h" + +static void +checkcrlf(struct ct *t) +{ + static char data[] = "crlf\tvalue\r\n"; + char path[80], *v; + Trie *trie; + int fd, n; + + snprint(path, sizeof path, "/tmp/strans-crlf-%d.map", getpid()); + remove(path); + fd = create(path, OWRITE, 0600); + if(fd < 0){ + CT_ERRORF(t, "cannot create CRLF fixture"); + return; + } + if(write(fd, data, sizeof data - 1) != (long)(sizeof data - 1)){ + CT_ERRORF(t, "cannot write CRLF fixture"); + close(fd); + remove(path); + return; + } + close(fd); + trie = trieopen(path); + remove(path); + v = trieget(trie, "crlf", 4, &n); + if(CT_CHECK(t, v != nil)){ + CT_EQ_INT(t, 5, n); + CT_EQ_MEM(t, "value", v, n); + } + trieclose(trie); +} + +void +trie_exact_prefix_and_duplicate(struct ct *t) +{ + char *v, *sentinel; + Trie *trie; + int n; + + trie = trieopen("data/trie.map"); + v = trieget(trie, "a", 1, &n); + if(!CT_CHECK(t, v != nil)) + goto cleanup; + CT_EQ_INT(t, 5, n); + CT_EQ_MEM(t, "alpha", v, n); + sentinel = "unchanged"; + v = sentinel; + n = 77; + CT_CHECK(t, trielookup(trie, "dupli", 5, &v, &n)); + CT_EQ_PTR(t, sentinel, v); + CT_EQ_INT(t, 77, n); + v = trieget(trie, "duplicate", 9, &n); + if(!CT_CHECK(t, v != nil)) + goto cleanup; + CT_EQ_INT(t, 6, n); + CT_EQ_MEM(t, "second", v, n); + v = trieget(trie, "tabs", 4, &n); + if(!CT_CHECK(t, v != nil)) + goto cleanup; + CT_EQ_INT(t, 7, n); + CT_EQ_MEM(t, "one\ttwo", v, n); + CT_EQ_PTR(t, nil, trieget(trie, "missing", 7, &n)); +cleanup: + trieclose(trie); + checkcrlf(t); +} + +void +production_maps_load(struct ct *t) +{ + char path[256]; + Trie *map; + int i, loaded, registered; + + loaded = registered = 0; + for(i = 0; i < nlang; i++){ + if(langs[i].mapname == nil) + continue; + registered++; + snprint(path, sizeof path, "../map/%s.map", langs[i].mapname); + map = trieopen(path); + if(!CT_CHECK(t, map != nil)) + continue; + trieclose(map); + loaded++; + } + CT_CHECK(t, registered > 0); + CT_EQ_INT(t, registered, loaded); +} + +void +transmap_states(struct ct *t) +{ + static const struct { + char *pre; + Rune key; + int eat; + char *emit; + char *next; + char *mapped; + } cases[] = { + { "", 'k', 1, "", "k", "" }, + { "k", 'a', 1, "", "ka", "か" }, + { "ka", 's', 1, "か", "s", "" }, + { "ka", 'q', 0, "か", "", "" }, + { "k", 'q', 0, "k", "", "" }, + }; + Emit e; + Im state; + int i; + + memset(&state, 0, sizeof state); + state.l = getlang(LangJP); + for(i = 0; i < nelem(cases); i++){ + state.pre = mkstr(cases[i].pre); + e = transmap(&state, cases[i].key); + if(e.eat != cases[i].eat) + CT_ERRORF(t, "case %d: want eat %d, got %d", + i, cases[i].eat, e.eat); + checkstr(t, "emit", cases[i].emit, &e.s); + checkstr(t, "next", cases[i].next, &e.next); + checkstr(t, "mapped", cases[i].mapped, &e.dict); + } +} diff --git a/tests/unit_test.c b/tests/unit_test.c new file mode 100644 index 0000000..e905635 --- /dev/null +++ b/tests/unit_test.c @@ -0,0 +1,112 @@ +#define CT_IMPLEMENTATION +#include "test.h" + +#include + +Channel *drawc; +Channel *keyc; +Channel *dictreqc; +Channel *dictresc; +Lang testvi; + +void +die(char *fmt, ...) +{ + va_list ap; + + fprint(2, "unit_test: "); + va_start(ap, fmt); + vfprint(2, fmt, ap); + va_end(ap); + fprint(2, "\n"); + threadexitsall("die"); +} + +void* +emalloc(ulong n) +{ + void *p; + + p = malloc(n); + if(p == nil) + die("out of memory"); + memset(p, 0, n); + return p; +} + +void* +erealloc(void *p, ulong n) +{ + p = realloc(p, n); + if(p == nil) + die("out of memory"); + return p; +} + +static void +testmapinit(void) +{ + Lang *jp; + + jp = getlang(LangJP); + if(jp == nil) + die("test language is not registered"); + jp->map = trieopen("data/hira.map"); + memset(&testvi, 0, sizeof testvi); + testvi.lang = LangVI; + testvi.mapname = "telex"; + testvi.trans = transvi; + testvi.back = backvi; + testvi.map = trieopen("../map/telex.map"); +} + +static const struct ct_test tests[] = { + { "str/init-utf8", str_init_utf8 }, + { "str/edit-and-alias", str_edit_and_alias }, + { "str/utf8-capacity", str_utf8_capacity }, + { "hmap/set-replace-grow", hmap_set_replace_and_grow }, + { "hmap/long-utf8-keys", hmap_long_utf8_keys }, + { "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate }, + { "map/production-lifecycle", production_maps_load }, + { "transmap/states", transmap_states }, + { "hangul/sequences", korean_sequences }, + { "hangul/backspace", korean_backspace }, + { "telex/transitions", vietnamese_transitions }, + { "telex/backspace", vietnamese_backspace }, + { "telex/state-lifetime", vietnamese_state_lifetime }, + { "engine/clears-candidates", engine_clears_candidates }, + { "engine/selects-visible-candidate", engine_selects_visible_candidate }, + { "engine/commit-contract", engine_commit_contract }, + { "engine/telex-history-bound", engine_telex_history_bound }, + { "dict/candidates", dictionary_candidates }, + { "ipc/request-codec", ipc_request_codec }, + { "ipc/response-pack-boundaries", ipc_response_pack_boundaries }, + { "ipc/response-codec-drain", ipc_response_codec_and_drain }, + { "ipc/truncated-frame", ipc_truncated_frame }, + { "ipc/fragmented-request", ipc_fragmented_request }, + { "ipc/closed-peer", ipc_closed_peer }, +}; + +void +threadmain(int argc, char **argv) +{ + int i, status; + + drawc = chancreate(sizeof(Drawcmd), 4); + keyc = chancreate(sizeof(Keyreq), 0); + dictreqc = chancreate(sizeof(Dictreq), 64); + dictresc = chancreate(sizeof(Dictres), 0); + testmapinit(); + status = CT_RUN_ARGS(tests, argc, argv); + for(i = 0; i < nlang; i++){ + trieclose(langs[i].map); + langs[i].map = nil; + } + trieclose(testvi.map); + testvi.map = nil; + chanfree(drawc); + chanfree(keyc); + chanfree(dictreqc); + chanfree(dictresc); + threadexitsall(status == 0 ? nil : "tests failed"); +} diff --git a/tests/vi_test.c b/tests/vi_test.c new file mode 100644 index 0000000..752c7ff --- /dev/null +++ b/tests/vi_test.c @@ -0,0 +1,84 @@ +#include "test.h" + +static void +typevi(struct ct *t, char *keys, char *want) +{ + Emit e; + Im state; + Str out, shown; + char *p; + + memset(&state, 0, sizeof state); + state.l = &testvi; + sclear(&out); + for(p = keys; *p != '\0'; p++){ + e = transvi(&state, (uchar)*p); + sappend(&out, &e.s); + state.pre = e.next; + if(!e.eat) + sputr(&out, (uchar)*p); + } + shown = shownpre(&state); + sappend(&out, &shown); + checkstr(t, keys, want, &out); +} + +void +vietnamese_transitions(struct ct *t) +{ + static const struct { char *keys, *want; } cases[] = { + { "as", "á" }, + { "As", "Á" }, + { "ans", "án" }, + { "Ans", "Án" }, + { "aws", "ắ" }, + { "asw", "ắ" }, + { "aww", "aw" }, + { "ddd", "dd" }, + { "ddas", "đá" }, + { "dds", "đs" }, + { "quas", "quá" }, + { "quaf", "quà" }, + { "quar", "quả" }, + { "quax", "quã" }, + { "quaj", "quạ" }, + { "Quas", "Quá" }, + { "gias", "giá" }, + { "Gias", "Giá" }, + { "gif", "gì" }, + { "\\s", "s" }, + { "a\\s", "as" }, + }; + int i; + + for(i = 0; i < nelem(cases); i++) + typevi(t, cases[i].keys, cases[i].want); +} + +void +vietnamese_backspace(struct ct *t) +{ + static const struct { + char *pre, *raw, *rawafter, *after; + } cases[] = { + { "as", "as", "a", "a" }, + { "quá", "quas", "qua", "qua" }, + { "à", "asf", "as", "á" }, + { "a", "asz", "as", "á" }, + { "quà", "quasf", "quas", "quá" }, + }; + Im state; + Str shown; + int i; + + for(i = 0; i < nelem(cases); i++){ + memset(&state, 0, sizeof state); + state.l = &testvi; + state.pre = mkstr(cases[i].pre); + state.raw = mkstr(cases[i].raw); + backvi(&state); + checkstr(t, cases[i].raw, cases[i].rawafter, &state.raw); + shown = shownpre(&state); + checkstr(t, cases[i].raw, cases[i].after, &shown); + } +}