strengthen core unit tests

Add table-driven dictionary miss and language-switch state coverage, regress stale candidates after backspace, and clear them before dictionary refresh. Keep IPC and runtime-created file checks outside the unit runner.
This commit is contained in:
2026-08-11 21:12:23 +09:00
parent 9b3f2c46f0
commit 82276bf37f
10 changed files with 149 additions and 464 deletions

View File

@@ -29,8 +29,8 @@ 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 the byte-stream IPC path. Tests use explicit case tables,
boundary values, and small regressions for repaired core contracts. GUI
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.
## Run

View File

@@ -86,11 +86,10 @@ dictqmap(Im *im)
{
Str dict;
if(!mapget(im->l->map, &im->pre, &dict)){
clearkouho();
show();
clearkouho();
show();
if(!mapget(im->l->map, &im->pre, &dict))
return;
}
dictsend(im, &dict);
}

View File

@@ -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 ipc_test.c
ko_test.c vi_test.c engine_test.c dict_test.c
TESTOBJ = $(TESTSRC:.c=.o)
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c srv.c
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
OBJS = $(TESTOBJ) $(PARENTOBJ)

View File

@@ -1,2 +1,4 @@
ka か
sa さ
n ん
na な

View File

@@ -51,3 +51,38 @@ cleanup:
hmapfree(lang->dict);
lang->dict = saved;
}
void
dictionary_misses_clear_result(struct ct *t)
{
static const struct {
char *name;
char *key;
char *pre;
} cases[] = {
{ "empty key", "", "empty-preedit" },
{ "missing key", "missing", "missing-preedit" },
};
Dictreq req;
Dictres res;
Hmap *saved;
Lang *lang;
int i;
lang = getlang(LangJP);
saved = lang->dict;
lang->dict = hmapalloc(1);
for(i = 0; i < nelem(cases); i++){
memset(&res, 0xa5, sizeof res);
req.key = mkstr(cases[i].key);
req.pre = mkstr(cases[i].pre);
req.lang = LangJP;
dictlookup(&req, &res);
if(res.nkouho != 0)
CT_ERRORF(t, "%s: want 0 candidates, got %d",
cases[i].name, res.nkouho);
checkstr(t, cases[i].name, cases[i].pre, &res.key);
}
hmapfree(lang->dict);
lang->dict = saved;
}

View File

@@ -54,6 +54,32 @@ engine_clears_candidates(struct ct *t)
CT_EQ_INT(t, -1, im.sel);
}
void
engine_backspace_clears_candidates(struct ct *t)
{
Lang *lang;
Str com, shown;
lang = getlang(LangJP);
init();
im.l = lang;
sclear(&com);
CT_CHECK(t, keystroke('n', 0, &com));
CT_CHECK(t, keystroke('a', 0, &com));
shown = shownpre(&im);
checkstr(t, "before backspace", "", &shown);
im.kouho[0] = mkstr("stale-candidate");
im.nkouho = 1;
im.sel = 0;
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspace commit", "", &com);
checkstr(t, "raw preedit after backspace", "n", &im.pre);
shown = shownpre(&im);
checkstr(t, "shown preedit after backspace", "", &shown);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
}
void
engine_selects_visible_candidate(struct ct *t)
{
@@ -156,3 +182,76 @@ engine_telex_history_bound(struct ct *t)
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
}
void
engine_language_switch_state(struct ct *t)
{
static const struct {
char *name;
Rune key;
int mod;
int lang;
char *pre;
char *raw;
char *commit;
int stale;
} steps[] = {
{ "select Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 },
{ "Vietnamese a", 'a', 0, LangVI, "a", "a", "", 0 },
{ "Vietnamese tone", 's', 0, LangVI, "as", "as", "", 0 },
{ "switch to Korean", 's', Mctrl, LangKO, "", "", "", 1 },
{ "Korean initial", 'r', 0, LangKO, "", "", "", 0 },
{ "Korean syllable", 'k', 0, LangKO, "", "", "", 0 },
{ "switch to Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 },
{ "Vietnamese a again", 'a', 0, LangVI, "a", "a", "", 0 },
{ "Vietnamese tone again", 's', 0, LangVI, "as", "as", "", 0 },
{ "commit Vietnamese", Kret, 0, LangVI, "", "", "á", 0 },
};
char where[80];
Lang *vi;
Trie *saved;
Str com;
int eaten, i;
vi = getlang(LangVI);
if(!CT_CHECK(t, vi != nil))
return;
saved = vi->map;
vi->map = testvi.map;
init();
for(i = 0; i < nelem(steps); i++){
if(steps[i].stale){
im.kouho[0] = mkstr("stale");
im.nkouho = 1;
im.sel = 0;
}
sclear(&com);
eaten = keystroke(steps[i].key, steps[i].mod, &com);
if(eaten != 1){
CT_ERRORF(t, "%s: want eaten 1, got %d",
steps[i].name, eaten);
break;
}
if(im.l == nil || im.l->lang != steps[i].lang){
CT_ERRORF(t, "%s: want language %d, got %d",
steps[i].name, steps[i].lang,
im.l == nil ? -1 : im.l->lang);
break;
}
snprint(where, sizeof where, "%s preedit", steps[i].name);
if(!checkstr(t, where, steps[i].pre, &im.pre))
break;
snprint(where, sizeof where, "%s raw", steps[i].name);
if(!checkstr(t, where, steps[i].raw, &im.raw))
break;
snprint(where, sizeof where, "%s commit", steps[i].name);
if(!checkstr(t, where, steps[i].commit, &com))
break;
if(im.nkouho != 0 || im.sel != -1){
CT_ERRORF(t, "%s: want candidates 0 and selection -1, got %d and %d",
steps[i].name, im.nkouho, im.sel);
break;
}
}
vi->map = saved;
}

View File

@@ -1,411 +0,0 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#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");
}
}

View File

@@ -28,15 +28,12 @@ void vietnamese_transitions(struct ct*);
void vietnamese_backspace(struct ct*);
void vietnamese_state_lifetime(struct ct*);
void engine_clears_candidates(struct ct*);
void engine_backspace_clears_candidates(struct ct*);
void engine_selects_visible_candidate(struct ct*);
void engine_commit_contract(struct ct*);
void engine_language_switch_state(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*);
void dictionary_misses_clear_result(struct ct*);
#endif

View File

@@ -1,37 +1,5 @@
#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)
{
@@ -64,7 +32,6 @@ trie_exact_prefix_and_duplicate(struct ct *t)
CT_EQ_PTR(t, nil, trieget(trie, "missing", 7, &n));
cleanup:
trieclose(trie);
checkcrlf(t);
}
void

View File

@@ -75,16 +75,13 @@ static const struct ct_test tests[] = {
{ "telex/backspace", vietnamese_backspace },
{ "telex/state-lifetime", vietnamese_state_lifetime },
{ "engine/clears-candidates", engine_clears_candidates },
{ "engine/backspace-clears-candidates", engine_backspace_clears_candidates },
{ "engine/selects-visible-candidate", engine_selects_visible_candidate },
{ "engine/commit-contract", engine_commit_contract },
{ "engine/language-switch-state", engine_language_switch_state },
{ "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 },
{ "dict/misses-clear-result", dictionary_misses_clear_result },
};
void