diff --git a/Makefile b/Makefile index 8d96084..71cd7bd 100644 --- a/Makefile +++ b/Makefile @@ -43,5 +43,6 @@ test: verify-map: python3 map/mktelex.py | cmp - map/telex.map python3 -B map/mkemoji | cmp - map/emoji.dict + python3 -B tests/mkemoji_test.py .PHONY: all check test verify-map clean xim bench diff --git a/README.md b/README.md index aea01e3..11af3cf 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ mirrors must provide the same sibling; initialize it after cloning with The suite covers the fixed-size UTF-8 string, hash table, trie fixtures, Japanese/Hangul/Telex state transitions, dictionary candidates, engine -selection state, and production map loading. Tests use explicit case tables, -boundary values, and small regressions for repaired core contracts. IPC and GUI -protocol stacks are outside this suite and require separate integration checks. +selection state, IPC request fields, and production map loading. Tests use +explicit case tables, boundary values, and small regressions for repaired core +contracts. Full IPC and GUI protocol stacks require separate integration checks. ## Run @@ -69,7 +69,7 @@ Switch input modes with Ctrl + key: P Toggle preedit echo Type romanized input. Select candidates with 1-9 or arrow keys. -Tab or Enter to commit. +Outside one-shot search, Tab or Enter commits. ### Emoji and symbols @@ -82,13 +82,17 @@ preedit echo. 안녕 → Ctrl+E → smile → Enter → 😀 There is no auto-insert. Enter commits the first candidate (or the current -one); arrows and Tab move, Alt+1 through Alt+9 select, Esc cancels. Plain -digits are part of the query. +one); arrows and Tab move, and Esc cancels. Bare 1 through 9 select an existing +numbered row; a bare digit with no such row and 0 remain query text. Digit +aliases are kept in matching slots, so `^1`, `_2`, and `<3` produce `¹`, `₂`, +and `♥`. `map/emoji.src` is the source of truth: each UTF-8 TSV row is a result followed -by one or more tab-separated aliases. Aliases may be multilingual. Run -`map/mkemoji > map/emoji.dict` after editing it; `make verify-map` checks the -generated dictionary and the Telex map. +by one or more tab-separated aliases. Aliases may be multilingual and ASCII +letters are case-insensitive. Results and aliases are limited to 64 Unicode +code points; results cannot contain whitespace. Run `map/mkemoji > map/emoji.dict` +after editing it; `make verify-map` checks the generated dictionary and the +Telex map. 😀 웃음 웃다 스마일 smile grin diff --git a/ipc.c b/ipc.c index 8bf166f..6537cb1 100644 --- a/ipc.c +++ b/ipc.c @@ -38,7 +38,7 @@ ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, unsigned int *mod, unsigned int *key) { *want = req[0] != 0; - *mod = req[1]; + *mod = req[1] & Mmask; *key = (unsigned int)req[2] | ((unsigned int)req[3] << 8) | ((unsigned int)req[4] << 16) | diff --git a/ipc.h b/ipc.h index 0242511..bf52867 100644 --- a/ipc.h +++ b/ipc.h @@ -7,6 +7,7 @@ /* * Request: [want-preedit, modifiers, key byte 0, ..., key byte 3]. + * Only Mmask modifier bits are retained. * Response: [eaten, commit-length-low, commit-length-high, commit...], * followed, when requested, by * [preedit-length-low, preedit-length-high, preedit...]. @@ -22,13 +23,14 @@ enum Kesc = Kspec|0x1b, Kup = Kspec|0x52, Kdown = Kspec|0x54, - Kshift = Kspec|0xe1, - Ksuper = Kspec|0xeb, + Kmodfirst = Kspec|0xe1, + Kmodlast = Kspec|0xee, Mshift = 1<<0, Mctrl = 1<<2, Malt = 1<<3, Msuper = 1<<6, + Mmask = Mshift|Mctrl|Malt|Msuper, Ipcreqsz = 6, Ipclensz = 2, diff --git a/map/emoji.dict b/map/emoji.dict index e1077b5..a64a504 100644 --- a/map/emoji.dict +++ b/map/emoji.dict @@ -14,7 +14,7 @@ : ☹ ☺ :( ☹ :) ☺ -< ← ≤ ≠ ♥ +< ← ≤ ♥ ≠ <- ← <3 ♥ <= ≤ @@ -24,7 +24,7 @@ => ⇒ > ≥ >= ≥ -^ ⁽ ⁾ ⁺ ⁻ ⁼ ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁱ ⁿ +^ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽ ⁾ ⁺ ⁻ ⁼ ⁰ ⁱ ⁿ ^( ⁽ ^) ⁾ ^+ ⁺ @@ -42,7 +42,7 @@ ^= ⁼ ^i ⁱ ^n ⁿ -_ ₍ ₎ ₊ ₋ ₌ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₐ ₑ ₒ ₓ +_ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍ ₎ ₊ ₋ ₌ ₀ ₐ ₑ ₒ ₓ _( ₍ _) ₎ _+ ₊ diff --git a/map/emoji.src b/map/emoji.src index e1c34fc..2b7a844 100644 --- a/map/emoji.src +++ b/map/emoji.src @@ -8,12 +8,34 @@ ÷ ./ ☹ :( ☺ :) + +# Bare 1-9 choose prefix candidates; keep digit aliases in matching slots. ← <- ≤ <= +♥ <3 ≠ <> != ≡ == ⇒ => ≥ >= +¹ ^1 +² ^2 +³ ^3 +⁴ ^4 +⁵ ^5 +⁶ ^6 +⁷ ^7 +⁸ ^8 +⁹ ^9 +₁ _1 +₂ _2 +₃ _3 +₄ _4 +₅ _5 +₆ _6 +₇ _7 +₈ _8 +₉ _9 + ⁽ ^( ⁾ ^) ⁺ ^+ @@ -27,25 +49,6 @@ ≈ ~= ⁰ ^0 ₀ _0 -¹ ^1 -₁ _1 -² ^2 -₂ _2 -♥ <3 -³ ^3 -₃ _3 -⁴ ^4 -₄ _4 -⁵ ^5 -₅ _5 -⁶ ^6 -₆ _6 -⁷ ^7 -₇ _7 -⁸ ^8 -₈ _8 -⁹ ^9 -₉ _9 ₐ _a α alpha β beta diff --git a/map/mkemoji b/map/mkemoji index 4925bfd..193378a 100755 --- a/map/mkemoji +++ b/map/mkemoji @@ -7,6 +7,7 @@ from pathlib import Path SOURCE = Path(__file__).with_name("emoji.src") +MAXRUNES = 64 def fold(s): @@ -14,22 +15,30 @@ def fold(s): return unicodedata.normalize("NFC", s) +def hascontrol(s): + return any(unicodedata.category(c) == "Cc" for c in s) + + def read(path): entries = [] with path.open(encoding="utf-8") as src: for lineno, raw in enumerate(src, 1): line = raw.rstrip("\r\n") - if not line.strip() or line.lstrip().startswith("#"): + if not line.strip(): + continue + if "\t" not in line and line.lstrip().startswith("#"): continue fields = line.split("\t") if len(fields) < 2: raise ValueError(f"{path}:{lineno}: need a result and an alias") result = unicodedata.normalize("NFC", fields[0]) - if not result or any(c.isspace() for c in result): + if (not result or len(result) > MAXRUNES or hascontrol(result) + or any(c.isspace() for c in result)): raise ValueError(f"{path}:{lineno}: bad result") for field in fields[1:]: alias = fold(field) - if not alias or alias != alias.strip(): + if (not alias or len(alias) > MAXRUNES or hascontrol(alias) + or alias != alias.strip()): raise ValueError(f"{path}:{lineno}: bad alias") entries.append((result, alias)) return entries diff --git a/strans.c b/strans.c index 3e0fc6c..577dda9 100644 --- a/strans.c +++ b/strans.c @@ -53,6 +53,7 @@ setkouho(Dictres *res) static void show(void) { + static Drawcmd old; Drawcmd dc; Str *pre; int i, first, n; @@ -73,7 +74,10 @@ show(void) if(dc.pre.n == 0 && n == 0 && !visible) return; visible = dc.pre.n != 0 || n != 0; - chansend(drawc, &dc); + /* imthread is the sole producer; keep only the newest pending draw. */ + while(channbrecv(drawc, &old) > 0) + ; + channbsend(drawc, &dc); } static void @@ -227,11 +231,19 @@ transmap(Im *im, Rune c) static Rune ctrlkey(Rune c) { + if(c >= 'A' && c <= 'Z') + c += 'a' - 'A'; if(c >= 'a' && c <= 'z') return c - 'a' + 1; return c; } +static int +ismodkey(u32int ks) +{ + return ks >= Kmodfirst && ks <= Kmodlast; +} + static void foldascii(Str *s) { @@ -369,8 +381,8 @@ searchkey(u32int ks, u32int mod, Str *com) int n, off; Rune c; - if(ks == Ksuper || ks == Kshift) - return 1; + if(ismodkey(ks)) + return 0; if(ks == Kdown || ks == Kup){ if(im.nkouho == 0) return 1; @@ -383,7 +395,7 @@ searchkey(u32int ks, u32int mod, Str *com) } n = ks - '1'; off = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0; - if((mod & Malt) && n >= 0 && n < Maxdisp){ + if(n >= 0 && n < Maxdisp && off + n < im.nkouho && mod == 0){ pickemoji(off + n, com); return 1; } @@ -454,8 +466,8 @@ keystroke(u32int ks, u32int mod, Str *com) int n, off; Rune c; - if(ks == Ksuper || ks == Kshift) - return 1; + if(ismodkey(ks)) + return 0; if(search.on) return searchkey(ks, mod, com); if(ks == Kdown || ks == Kup){ @@ -568,6 +580,16 @@ imhandlekey(Keyreq *kr) chansend(kr->reply, &res); } +static void +dictresult(Dictres *res) +{ + if(search.on || res->lang != im.l->lang || + scmp(&res->key, &im.pre) != 0) + return; + setkouho(res); + show(); +} + void imthread(void*) { @@ -587,11 +609,7 @@ imthread(void*) imhandlekey(&kr); break; case 1: - if(!search.on && res.lang == im.l->lang && - scmp(&res.key, &im.pre) == 0){ - setkouho(&res); - show(); - } + dictresult(&res); break; } } diff --git a/tests/Makefile b/tests/Makefile index 3cebdc9..e02090c 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -5,9 +5,9 @@ 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 + 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 +PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) OBJS = $(TESTOBJ) $(PARENTOBJ) diff --git a/tests/engine_test.c b/tests/engine_test.c index 3402d36..6f57378 100644 --- a/tests/engine_test.c +++ b/tests/engine_test.c @@ -302,11 +302,19 @@ emojibegin(Emojifix *f, int showpre) f->popup = popup; f->visible = visible; draindraw(nil); - l->dict = hmapalloc(8); + l->dict = hmapalloc(32); + setemoji(&l->dict, "a", "A B"); + setemoji(&l->dict, "ㅁ", "B C"); setemoji(&l->dict, "alpha", "α"); setemoji(&l->dict, "smil", "😀 😄"); setemoji(&l->dict, "smile", "😀 😄"); setemoji(&l->dict, "웃음", "😀 😄 😂"); + setemoji(&l->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽"); + setemoji(&l->dict, "^0", "⁰"); + setemoji(&l->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍"); + setemoji(&l->dict, "<", "← ≤ ♥ ≠"); + setemoji(&l->dict, "many", + "c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12"); init(); im.l = getlang(LangKO); popup = showpre; @@ -345,8 +353,10 @@ engine_emoji_single_candidate(struct ct *t) Emojifix f; Str com; - emojibegin(&f, 0); + emojibegin(&f, 1); sclear(&com); + CT_CHECK(t, keystroke('p', Mctrl, &com)); + CT_EQ_INT(t, 0, popup); CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, search.on); CT_EQ_INT(t, LangKO, im.l->lang); @@ -363,6 +373,10 @@ engine_emoji_single_candidate(struct ct *t) checkstr(t, "explicit candidate commit", "α", &com); CT_CHECK(t, !search.on); CT_EQ_INT(t, LangKO, im.l->lang); + show(); + CT_CHECK(t, draindraw(&dc) > 0); + CT_EQ_INT(t, 0, dc.pre.n); + CT_EQ_INT(t, 0, dc.nkouho); } emojiend(&f); } @@ -376,6 +390,18 @@ engine_emoji_queries(struct ct *t) emojibegin(&f, 1); sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + CT_CHECK(t, keystroke('a', 0, &com)); + checkstr(t, "raw query wins", "a", &search.text); + CT_EQ_INT(t, 3, im.nkouho); + checkstr(t, "raw candidate", "A", &im.kouho[0]); + checkstr(t, "deduplicated candidate", "B", &im.kouho[1]); + checkstr(t, "local candidate", "C", &im.kouho[2]); + CT_CHECK(t, draindraw(&dc) > 0); + checkstr(t, "prefix popup query", "a", &dc.pre); + CT_EQ_INT(t, 3, dc.nkouho); + CT_CHECK(t, keystroke(Kesc, 0, &com)); + CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "smile", &com)){ checkstr(t, "English alias", "smile", &search.text); @@ -384,11 +410,19 @@ engine_emoji_queries(struct ct *t) checkstr(t, "backspaced query", "smil", &search.text); CT_EQ_INT(t, 2, im.nkouho); CT_CHECK(t, keystroke('e', 0, &com)); - CT_CHECK(t, keystroke('2', Malt, &com)); + CT_CHECK(t, keystroke('2', 0, &com)); checkstr(t, "selected English alias", "😄", &com); CT_EQ_INT(t, LangKO, im.l->lang); } + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "SMILE", &com)){ + checkstr(t, "folded English alias", "smile", &search.text); + CT_EQ_INT(t, 2, im.nkouho); + CT_CHECK(t, keystroke(Kesc, 0, &com)); + } + sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "dntdma", &com)){ @@ -405,11 +439,187 @@ engine_emoji_queries(struct ct *t) emojiend(&f); } +void +engine_emoji_digit_aliases(struct ct *t) +{ + static const struct { + char *name; + Rune prefix; + Rune digit; + char *want; + } cases[] = { + { "^1", '^', '1', "¹" }, + { "_2", '_', '2', "₂" }, + { "<3", '<', '3', "♥" }, + }; + Emojifix f; + Str com; + int i; + + emojibegin(&f, 0); + for(i = 0; i < nelem(cases); i++){ + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + CT_CHECK(t, keystroke(cases[i].prefix, Mshift, &com)); + CT_CHECK(t, im.nkouho >= cases[i].digit - '0'); + CT_CHECK(t, keystroke(cases[i].digit, 0, &com)); + checkstr(t, cases[i].name, cases[i].want, &com); + CT_CHECK(t, !search.on); + CT_EQ_INT(t, LangKO, im.l->lang); + } + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + CT_CHECK(t, keystroke('^', Mshift, &com)); + CT_CHECK(t, keystroke('0', 0, &com)); + checkstr(t, "zero query", "^0", &search.text); + checkstr(t, "zero does not commit", "", &com); + CT_EQ_INT(t, 1, im.nkouho); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "zero alias", "⁰", &com); + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "smile", &com)){ + CT_CHECK(t, keystroke('9', 0, &com)); + checkstr(t, "missing number query", "smile9", &search.text); + checkstr(t, "missing number commit", "", &com); + CT_CHECK(t, search.on); + CT_EQ_INT(t, 0, im.nkouho); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "missing number text", "smile9", &com); + } + emojiend(&f); +} + +void +engine_emoji_navigation(struct ct *t) +{ + Drawcmd dc = {0}; + Emojifix f; + Str com; + int i; + + emojibegin(&f, 0); + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "many", &com)){ + CT_EQ_INT(t, 12, im.nkouho); + for(i = 0; i < Maxdisp+1; i++) + CT_CHECK(t, keystroke(Kdown, 0, &com)); + CT_EQ_INT(t, Maxdisp, im.sel); + CT_CHECK(t, draindraw(&dc) > 0); + checkstr(t, "scrolled first row", "c2", &dc.kouho[0]); + checkstr(t, "scrolled ninth row", "c10", &dc.kouho[8]); + CT_EQ_INT(t, 8, dc.sel); + CT_CHECK(t, keystroke('1', 0, &com)); + checkstr(t, "scrolled row one", "c2", &com); + } + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "many", &com)){ + for(i = 0; i < 12; i++) + CT_CHECK(t, keystroke(Kdown, 0, &com)); + CT_EQ_INT(t, 11, im.sel); + CT_CHECK(t, keystroke('9', 0, &com)); + checkstr(t, "scrolled row nine", "c12", &com); + } + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "smile", &com)){ + CT_CHECK(t, keystroke(Ktab, 0, &com)); + CT_EQ_INT(t, 0, im.sel); + CT_CHECK(t, keystroke(Kdown, 0, &com)); + CT_EQ_INT(t, 1, im.sel); + CT_CHECK(t, keystroke(Kup, 0, &com)); + CT_EQ_INT(t, 0, im.sel); + CT_CHECK(t, keystroke(Ktab, Mshift, &com)); + CT_EQ_INT(t, 1, im.sel); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "navigated candidate", "😄", &com); + } + emojiend(&f); +} + +void +engine_emoji_preedit_languages(struct ct *t) +{ + Emojifix f; + Str com, shown; + int p; + + emojibegin(&f, 0); + init(); + im.l = getlang(LangJP); + sclear(&com); + if(typekeys(t, "ka", &com)){ + CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com)); + checkstr(t, "modifier preserves Japanese", "ka", &im.pre); + CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); + checkstr(t, "committed Japanese preedit", "か", &com); + CT_CHECK(t, search.on); + CT_EQ_INT(t, LangJP, im.l->lang); + CT_CHECK(t, keystroke('a', 0, &com)); + p = popup; + CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com)); + CT_CHECK(t, search.on); + checkstr(t, "modifier preserves search", "a", &search.text); + CT_CHECK(t, keystroke('P', Mctrl|Mshift, &com)); + CT_EQ_INT(t, !p, popup); + CT_CHECK(t, search.on); + CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com)); + CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); + CT_CHECK(t, !search.on); + sclear(&com); + if(typekeys(t, "na", &com)){ + shown = shownpre(&im); + checkstr(t, "Japanese resumed", "な", &shown); + } + } + + init(); + im.l = getlang(LangKO); + sclear(&com); + if(typekeys(t, "rk", &com)){ + CT_CHECK(t, keystroke('e', Mctrl, &com)); + checkstr(t, "committed Korean preedit", "가", &com); + if(typekeys(t, "smile", &com)){ + CT_CHECK(t, keystroke('1', 0, &com)); + checkstr(t, "Korean selection", "가😀", &com); + sclear(&com); + if(typekeys(t, "rk", &com)) + checkstr(t, "Korean resumed", "가", &im.pre); + } + } + + init(); + im.l = &testvi; + sclear(&com); + if(typekeys(t, "as", &com)){ + CT_CHECK(t, keystroke('e', Mctrl, &com)); + checkstr(t, "committed Telex preedit", "á", &com); + CT_CHECK(t, keystroke(Kesc, 0, &com)); + CT_CHECK(t, !search.on); + CT_EQ_PTR(t, &testvi, im.l); + sclear(&com); + if(typekeys(t, "as", &com)){ + shown = shownpre(&im); + checkstr(t, "Telex resumed", "á", &shown); + checkstr(t, "Telex raw resumed", "as", &im.raw); + } + } + emojiend(&f); +} + void engine_emoji_start_and_unknown(struct ct *t) { Emojifix f; Str com; + char want[Maxrunes+1]; + int i; emojibegin(&f, 0); sclear(&com); @@ -427,5 +637,67 @@ engine_emoji_start_and_unknown(struct ct *t) CT_EQ_INT(t, LangKO, im.l->lang); } } + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "xyz", &com)){ + CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com)); + checkstr(t, "special commits shown query", "xyz", &com); + CT_CHECK(t, !search.on); + CT_EQ_INT(t, LangKO, im.l->lang); + } + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + for(i = 0; i < Maxrunes; i++) + CT_CHECK(t, keystroke('q', 0, &com)); + CT_EQ_INT(t, Maxrunes, search.raw.n); + CT_CHECK(t, search.on); + CT_CHECK(t, !keystroke('z', 0, &com)); + memset(want, 'q', Maxrunes); + want[Maxrunes] = '\0'; + checkstr(t, "bounded query commit", want, &com); + CT_CHECK(t, !search.on); + emojiend(&f); +} + +void +engine_emoji_dictionary_identity(struct ct *t) +{ + Dictres res; + Emojifix f; + + emojibegin(&f, 0); + im.pre = mkstr("same"); + im.kouho[0] = mkstr("old"); + im.nkouho = 1; + im.sel = 0; + memset(&res, 0, sizeof res); + res.key = im.pre; + res.kouho[0] = mkstr("wrong"); + res.nkouho = 1; + res.lang = LangJP; + dictresult(&res); + CT_EQ_INT(t, 1, im.nkouho); + checkstr(t, "wrong language ignored", "old", &im.kouho[0]); + + res.lang = LangKO; + res.key = mkstr("other"); + dictresult(&res); + CT_EQ_INT(t, 1, im.nkouho); + checkstr(t, "wrong preedit ignored", "old", &im.kouho[0]); + + res.key = im.pre; + search.on = 1; + dictresult(&res); + CT_EQ_INT(t, 1, im.nkouho); + checkstr(t, "search response ignored", "old", &im.kouho[0]); + + search.on = 0; + res.kouho[0] = mkstr("right"); + dictresult(&res); + CT_EQ_INT(t, 1, im.nkouho); + CT_EQ_INT(t, -1, im.sel); + checkstr(t, "matching response accepted", "right", &im.kouho[0]); emojiend(&f); } diff --git a/tests/ipc_test.c b/tests/ipc_test.c new file mode 100644 index 0000000..e6dfb86 --- /dev/null +++ b/tests/ipc_test.c @@ -0,0 +1,15 @@ +#include "test.h" + +void +ipc_masks_modifiers(struct ct *t) +{ + uchar buf[Ipcreqsz]; + u32int key, mod; + int want; + + ipcpackreq(buf, 1, Mctrl|Malt|(1<<1)|(1<<4)|(1<<7), 0x1f642); + ipcunpackreq(buf, &want, &mod, &key); + CT_EQ_INT(t, 1, want); + CT_EQ_UINT(t, Mctrl|Malt, mod); + CT_EQ_UINT(t, 0x1f642, key); +} diff --git a/tests/mkemoji_test.py b/tests/mkemoji_test.py new file mode 100644 index 0000000..60d46ba --- /dev/null +++ b/tests/mkemoji_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MKEMOJI = ROOT / "map" / "mkemoji" + + +def generate(source=None): + args = [sys.executable, "-B", str(MKEMOJI)] + if source is not None: + args.append(str(source)) + return subprocess.run(args, capture_output=True, text=True, check=False) + + +def table(output): + return dict(line.split("\t", 1) for line in output.splitlines()) + + +class MkemojiTest(unittest.TestCase): + def source(self, text): + tmp = tempfile.TemporaryDirectory() + path = Path(tmp.name) / "emoji.src" + path.write_text(text, encoding="utf-8") + self.addCleanup(tmp.cleanup) + return path + + def test_production_digit_slots(self): + result = generate() + self.assertEqual(result.returncode, 0, result.stderr) + data = table(result.stdout) + self.assertEqual(data["^"].split()[:9], list("¹²³⁴⁵⁶⁷⁸⁹")) + self.assertEqual(data["_"].split()[:9], list("₁₂₃₄₅₆₇₈₉")) + self.assertEqual(data["<"].split()[2], "♥") + self.assertEqual(data["^1"], "¹") + self.assertEqual(data["_2"], "₂") + self.assertEqual(data["<3"], "♥") + + def test_fold_normalize_and_exact_first(self): + source = self.source( + "β\tALPHABET\n" + "α\talpha\n" + "e\u0301\tE\u0301\n" + "#\thash\n" + ) + first = generate(source) + second = generate(source) + self.assertEqual(first.returncode, 0, first.stderr) + self.assertEqual(first.stdout, second.stdout) + data = table(first.stdout) + self.assertEqual(data["alpha"].split(), ["α", "β"]) + self.assertEqual(data["al"].split(), ["β", "α"]) + self.assertEqual(data["é"], "é") + self.assertEqual(data["hash"], "#") + + def test_rejects_malformed_and_oversized_rows(self): + bad = [ + "x\n", + "bad result\ta\n", + "x\t\n", + "x\t a\n", + "\0\ta\n", + "x\ta\0b\n", + "x" * 65 + "\ta\n", + "x\t" + "a" * 65 + "\n", + ] + for text in bad: + with self.subTest(text=repr(text)): + result = generate(self.source(text)) + self.assertEqual(result.returncode, 1) + + def test_accepts_runtime_boundary(self): + source = self.source("x" * 64 + "\t" + "a" * 64 + "\n") + result = generate(source) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("a" * 64 + "\t" + "x" * 64, result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test.h b/tests/test.h index a2d57d8..95e4ac7 100644 --- a/tests/test.h +++ b/tests/test.h @@ -35,8 +35,13 @@ void engine_language_switch_state(struct ct*); void engine_telex_history_bound(struct ct*); void engine_emoji_single_candidate(struct ct*); void engine_emoji_queries(struct ct*); +void engine_emoji_digit_aliases(struct ct*); +void engine_emoji_navigation(struct ct*); +void engine_emoji_preedit_languages(struct ct*); void engine_emoji_start_and_unknown(struct ct*); +void engine_emoji_dictionary_identity(struct ct*); void dictionary_candidates(struct ct*); void dictionary_misses_clear_result(struct ct*); +void ipc_masks_modifiers(struct ct*); #endif diff --git a/tests/unit_test.c b/tests/unit_test.c index befe703..8ad4a09 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -82,9 +82,14 @@ static const struct ct_test tests[] = { { "engine/telex-history-bound", engine_telex_history_bound }, { "engine/emoji-single-candidate", engine_emoji_single_candidate }, { "engine/emoji-queries", engine_emoji_queries }, + { "engine/emoji-digit-aliases", engine_emoji_digit_aliases }, + { "engine/emoji-navigation", engine_emoji_navigation }, + { "engine/emoji-preedit-languages", engine_emoji_preedit_languages }, { "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown }, + { "engine/emoji-dictionary-identity", engine_emoji_dictionary_identity }, { "dict/candidates", dictionary_candidates }, { "dict/misses-clear-result", dictionary_misses_clear_result }, + { "ipc/masks-modifiers", ipc_masks_modifiers }, }; void @@ -92,7 +97,7 @@ threadmain(int argc, char **argv) { int i, status; - drawc = chancreate(sizeof(Drawcmd), 1024); + drawc = chancreate(sizeof(Drawcmd), 4); keyc = chancreate(sizeof(Keyreq), 0); dictreqc = chancreate(sizeof(Dictreq), 64); dictresc = chancreate(sizeof(Dictres), 0);