add focused unit and map checks
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "cutest"]
|
||||
path = cutest
|
||||
url = ../cutest.git
|
||||
25
Makefile
25
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
|
||||
|
||||
24
README.md
24
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
|
||||
|
||||
|
||||
1
cutest
Submodule
1
cutest
Submodule
Submodule cutest added at 7894ba7a26
34
tests/Makefile
Normal file
34
tests/Makefile
Normal file
@@ -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
|
||||
2
tests/data/hira.map
Normal file
2
tests/data/hira.map
Normal file
@@ -0,0 +1,2 @@
|
||||
ka か
|
||||
sa さ
|
||||
6
tests/data/trie.map
Normal file
6
tests/data/trie.map
Normal file
@@ -0,0 +1,6 @@
|
||||
a alpha
|
||||
ab beta
|
||||
한 값
|
||||
duplicate first
|
||||
duplicate second
|
||||
tabs one two
|
||||
53
tests/dict_test.c
Normal file
53
tests/dict_test.c
Normal file
@@ -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;
|
||||
}
|
||||
158
tests/engine_test.c
Normal file
158
tests/engine_test.c
Normal file
@@ -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);
|
||||
}
|
||||
78
tests/hash_test.c
Normal file
78
tests/hash_test.c
Normal file
@@ -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);
|
||||
}
|
||||
411
tests/ipc_test.c
Normal file
411
tests/ipc_test.c
Normal file
@@ -0,0 +1,411 @@
|
||||
#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");
|
||||
}
|
||||
}
|
||||
60
tests/ko_test.c
Normal file
60
tests/ko_test.c
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
84
tests/str_test.c
Normal file
84
tests/str_test.c
Normal file
@@ -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]);
|
||||
}
|
||||
42
tests/test.h
Normal file
42
tests/test.h
Normal file
@@ -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
|
||||
32
tests/test_util.c
Normal file
32
tests/test_util.c
Normal file
@@ -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;
|
||||
}
|
||||
126
tests/trie_test.c
Normal file
126
tests/trie_test.c
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
112
tests/unit_test.c
Normal file
112
tests/unit_test.c
Normal file
@@ -0,0 +1,112 @@
|
||||
#define CT_IMPLEMENTATION
|
||||
#include "test.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
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");
|
||||
}
|
||||
84
tests/vi_test.c
Normal file
84
tests/vi_test.c
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user