mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-20 02:18:08 +00:00
feat(dictionary): build a test wordlist without the upstream download
build-dictionary gains a --words mode that reads a plain list instead of the upstream database. Everything after that — filtering, alias generation, writing and verification — is the code the real build uses, so a fixture cannot drift into being shaped differently from what the server loads. testdata/fixture-words.txt is hand-written rather than extracted, so no test artifact carries the upstream release's licence. Its graph is built around one hub syllable: it is the only one with enough continuations for the server to open a game on, so every game starts on a word ending in it and a scripted test always knows the first answer.
This commit is contained in:
1 parent
aea9ea8cc0
commit
4277c2014c
3 files changed
+328
-3
No files matched your search
@@ -9,20 +9,24 @@ DICT_SRC := data/dictionary.db
|
||||
# changed under the same tag, or the download was truncated.
|
||||
DICT_SHA256 := 9259403f0675b2991a1bd0ef6d0dbc5933afdb135632af095a60662f09bbf1d3
|
||||
DICT_OUT := data/noitu.db
|
||||
FIXTURE_WORDS := testdata/fixture-words.txt
|
||||
FIXTURE_DB := data/fixture.db
|
||||
SERVER_BIN := noitu-server
|
||||
|
||||
.PHONY: help fetch-dict verify-dict dict proto proto-check server web web-dev test test-go test-web run clean
|
||||
.PHONY: help fetch-dict verify-dict dict fixture-dict proto proto-check server web web-dev test test-go test-web test-e2e run clean
|
||||
|
||||
help:
|
||||
@echo "fetch-dict download + checksum the upstream dictionary (~179 MB) into data/"
|
||||
@echo "verify-dict re-check the downloaded dictionary against its pinned SHA-256"
|
||||
@echo "dict derive $(DICT_OUT) from $(DICT_SRC)"
|
||||
@echo "fixture-dict build the small test dictionary — no download needed"
|
||||
@echo "proto regenerate the Go and JS wire types from proto/"
|
||||
@echo "proto-check lint the schema and verify the committed output is in sync"
|
||||
@echo "server build the Go server binary"
|
||||
@echo "web build the SvelteKit frontend"
|
||||
@echo "web-dev run the frontend dev server, proxying /ws to a local server"
|
||||
@echo "test run all tests"
|
||||
@echo "test-e2e run the Playwright suite against the fixture dictionary"
|
||||
@echo "run build and run the server locally"
|
||||
@echo "clean remove build artifacts (keeps downloaded dictionary)"
|
||||
|
||||
@@ -44,6 +48,14 @@ $(DICT_SRC):
|
||||
dict: $(DICT_SRC)
|
||||
cd server && go run ./cmd/build-dictionary --in ../$(DICT_SRC) --out ../$(DICT_OUT)
|
||||
|
||||
# The dictionary tests, end-to-end runs and CI all play against. Built from a
|
||||
# checked-in word list through the same pipeline as the real one, so nothing
|
||||
# has to download 179 MB to get a working database.
|
||||
$(FIXTURE_DB): $(FIXTURE_WORDS)
|
||||
cd server && go run ./cmd/build-dictionary --words ../$(FIXTURE_WORDS) --out ../$(FIXTURE_DB) --min-words 150
|
||||
|
||||
fixture-dict: $(FIXTURE_DB)
|
||||
|
||||
# Regenerates both targets from proto/noitu/v1/game.proto. Needs `buf`; the
|
||||
# two code generators come from server/go.mod's tool directive and
|
||||
# web/package.json, so there is nothing to install globally. Generated code is
|
||||
@@ -86,9 +98,12 @@ test-go:
|
||||
test-web: web/node_modules
|
||||
cd web && npm run check && npm test
|
||||
|
||||
test-e2e: web/node_modules $(FIXTURE_DB) server
|
||||
cd web && npm run test:e2e
|
||||
|
||||
run: server
|
||||
./$(SERVER_BIN)
|
||||
|
||||
clean:
|
||||
rm -f $(SERVER_BIN) $(SERVER_BIN).exe $(DICT_OUT)
|
||||
rm -f $(SERVER_BIN) $(SERVER_BIN).exe $(DICT_OUT) $(FIXTURE_DB)
|
||||
rm -rf web/build web/.svelte-kit
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -35,7 +36,10 @@ const (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
in string
|
||||
in string
|
||||
// words is an alternative source: a plain list, one word per line, used to
|
||||
// build a small fixture database without the upstream download.
|
||||
words string
|
||||
out string
|
||||
maxSyllables int
|
||||
minWords int
|
||||
@@ -50,6 +54,7 @@ func main() {
|
||||
|
||||
var cfg config
|
||||
flag.StringVar(&cfg.in, "in", "../data/dictionary.db", "upstream dictionary.db to read")
|
||||
flag.StringVar(&cfg.words, "words", "", "read a plain word list instead of a source database (one word per line, # comments)")
|
||||
flag.StringVar(&cfg.out, "out", "../data/noitu.db", "derived database to write")
|
||||
flag.IntVar(&cfg.maxSyllables, "max-syllables", 0, "reject words longer than this (0 = no limit)")
|
||||
flag.IntVar(&cfg.minWords, "min-words", 40000, "fail if fewer words survive filtering")
|
||||
@@ -65,6 +70,9 @@ func main() {
|
||||
}
|
||||
|
||||
func run(cfg config) error {
|
||||
if cfg.words != "" {
|
||||
return runFromWordList(cfg)
|
||||
}
|
||||
if _, err := os.Stat(cfg.in); err != nil {
|
||||
return fmt.Errorf("source database not found at %s — run 'make fetch-dict' first: %w", cfg.in, err)
|
||||
}
|
||||
@@ -107,6 +115,66 @@ func run(cfg config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// runFromWordList derives a database from a plain list of words instead of the
|
||||
// upstream release.
|
||||
//
|
||||
// It exists so tests and CI have a real dictionary to play against without the
|
||||
// 179 MB download. The filtering, alias generation, writing and verification
|
||||
// below are the same functions the real build uses — only the source of the
|
||||
// raw strings differs — so a fixture cannot drift into being shaped
|
||||
// differently from what production loads.
|
||||
func runFromWordList(cfg config) error {
|
||||
raw, err := os.ReadFile(cfg.words)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read word list: %w", err)
|
||||
}
|
||||
|
||||
words := make(map[string]entry)
|
||||
rejects := make(map[rejectReason]int)
|
||||
|
||||
for line := range strings.Lines(string(raw)) {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
word, syllables, reason, ok := accept(line, cfg.maxSyllables)
|
||||
if !ok {
|
||||
rejects[reason]++
|
||||
continue
|
||||
}
|
||||
words[word] = entry{
|
||||
word: word,
|
||||
first: syllables[0],
|
||||
last: syllables[len(syllables)-1],
|
||||
syllables: len(syllables),
|
||||
}
|
||||
}
|
||||
|
||||
logRejects(rejects)
|
||||
log.Printf("accepted %d distinct words from %s", len(words), cfg.words)
|
||||
|
||||
if len(words) < cfg.minWords {
|
||||
return fmt.Errorf("only %d words survived filtering, expected at least %d", len(words), cfg.minWords)
|
||||
}
|
||||
|
||||
aliases, collisions := buildAliases(words)
|
||||
log.Printf("generated %d spelling aliases (%d skipped as ambiguous or already real words)", len(aliases), collisions)
|
||||
|
||||
// The source spec is what lands in the meta table. Naming the list rather
|
||||
// than a table makes it obvious in the output which build produced a given
|
||||
// database.
|
||||
src := sourceSpec{table: "wordlist:" + filepath.Base(cfg.words)}
|
||||
if err := write(cfg.out, words, aliases, src); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := verify(cfg.out, cfg.minWords); err != nil {
|
||||
return fmt.Errorf("output failed verification: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("wrote %s", cfg.out)
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify re-opens the finished database and re-checks the invariants the game
|
||||
// depends on. The in-memory checks above can only prove what the builder
|
||||
// intended; this proves what actually landed on disk.
|
||||
|
||||
Vendored
+242
@@ -0,0 +1,242 @@
|
||||
# Fixture wordlist for tests, end-to-end runs and CI.
|
||||
#
|
||||
# Hand-written common Vietnamese compounds, not an extract of the upstream
|
||||
# dictionary: CI must never download the 179 MB release, and a fixture derived
|
||||
# from it would carry that release's licence into every test artifact.
|
||||
#
|
||||
# `make fixture-dict` turns this into a real derived database through the same
|
||||
# filter, alias and write path production uses, so a fixture cannot drift into
|
||||
# being shaped differently from what the server actually loads.
|
||||
#
|
||||
# The graph is built around one hub syllable, "sinh". It is the only syllable
|
||||
# with enough continuations to be chosen as an opening, so every game starts on
|
||||
# a word ending in "sinh" and a scripted test always knows the first answer.
|
||||
|
||||
# --- openings: words ending in the hub syllable ---
|
||||
học sinh
|
||||
thí sinh
|
||||
vệ sinh
|
||||
phát sinh
|
||||
khai sinh
|
||||
tái sinh
|
||||
dân sinh
|
||||
nhân sinh
|
||||
ký sinh
|
||||
chúng sinh
|
||||
|
||||
# --- the hub: words starting with "sinh" ---
|
||||
sinh viên
|
||||
sinh sản
|
||||
sinh hoạt
|
||||
sinh học
|
||||
sinh nhật
|
||||
sinh vật
|
||||
sinh tồn
|
||||
sinh thái
|
||||
sinh lý
|
||||
sinh kế
|
||||
sinh khí
|
||||
sinh mệnh
|
||||
sinh trưởng
|
||||
sinh động
|
||||
sinh sống
|
||||
sinh thành
|
||||
sinh lực
|
||||
sinh quán
|
||||
sinh sôi
|
||||
sinh nở
|
||||
sinh trắc
|
||||
sinh hóa
|
||||
|
||||
# --- continuations ---
|
||||
viên chức
|
||||
viên mãn
|
||||
chức năng
|
||||
chức vụ
|
||||
năng lực
|
||||
năng suất
|
||||
mãn nguyện
|
||||
nguyện vọng
|
||||
vọng tưởng
|
||||
|
||||
sản xuất
|
||||
sản phẩm
|
||||
sản lượng
|
||||
xuất bản
|
||||
xuất phát
|
||||
phẩm chất
|
||||
lượng giác
|
||||
bản đồ
|
||||
bản sắc
|
||||
đồ thị
|
||||
sắc thái
|
||||
|
||||
hoạt động
|
||||
hoạt bát
|
||||
động vật
|
||||
động cơ
|
||||
động lực
|
||||
động đất
|
||||
cơ bản
|
||||
cơ hội
|
||||
hội nghị
|
||||
nghị luận
|
||||
đất nước
|
||||
nước ngoài
|
||||
ngoài trời
|
||||
trời đất
|
||||
|
||||
học tập
|
||||
học phí
|
||||
học hỏi
|
||||
học bổng
|
||||
tập trung
|
||||
tập thể
|
||||
trung tâm
|
||||
tâm hồn
|
||||
hồn nhiên
|
||||
nhiên liệu
|
||||
liệu pháp
|
||||
pháp luật
|
||||
pháp lý
|
||||
luật sư
|
||||
sư phạm
|
||||
phạm vi
|
||||
vi phạm
|
||||
|
||||
nhật ký
|
||||
nhật báo
|
||||
báo cáo
|
||||
cáo trạng
|
||||
trạng thái
|
||||
thái độ
|
||||
thái bình
|
||||
độ cao
|
||||
cao nguyên
|
||||
bình minh
|
||||
minh bạch
|
||||
bạch tuộc
|
||||
|
||||
vật chất
|
||||
vật liệu
|
||||
chất lượng
|
||||
|
||||
tồn tại
|
||||
tồn kho
|
||||
tại chỗ
|
||||
kho tàng
|
||||
tàng hình
|
||||
hình ảnh
|
||||
ảnh hưởng
|
||||
hưởng thụ
|
||||
|
||||
lý do
|
||||
lý thuyết
|
||||
lý luận
|
||||
lý tưởng
|
||||
thuyết minh
|
||||
luận điểm
|
||||
tưởng tượng
|
||||
điểm danh
|
||||
danh sách
|
||||
sách vở
|
||||
tượng trưng
|
||||
|
||||
kế hoạch
|
||||
kế toán
|
||||
kế tiếp
|
||||
hoạch định
|
||||
toán học
|
||||
tiếp tục
|
||||
định hướng
|
||||
tục ngữ
|
||||
hướng dẫn
|
||||
ngữ pháp
|
||||
dẫn chứng
|
||||
chứng minh
|
||||
|
||||
khí hậu
|
||||
khí quyển
|
||||
khí thế
|
||||
hậu quả
|
||||
quyển sách
|
||||
thế giới
|
||||
quả cảm
|
||||
giới hạn
|
||||
hạn chế
|
||||
chế độ
|
||||
|
||||
mệnh lệnh
|
||||
mệnh đề
|
||||
lệnh cấm
|
||||
đề tài
|
||||
cấm vận
|
||||
tài liệu
|
||||
vận động
|
||||
|
||||
trưởng thành
|
||||
trưởng phòng
|
||||
thành phố
|
||||
thành công
|
||||
thành lập
|
||||
thành viên
|
||||
phòng ban
|
||||
phố cổ
|
||||
công việc
|
||||
công nghiệp
|
||||
lập trình
|
||||
việc làm
|
||||
nghiệp vụ
|
||||
trình độ
|
||||
làm việc
|
||||
cổ điển
|
||||
điển hình
|
||||
|
||||
sống động
|
||||
lực lượng
|
||||
lực sĩ
|
||||
lượng tử
|
||||
sĩ quan
|
||||
tử tế
|
||||
quan hệ
|
||||
hệ thống
|
||||
thống nhất
|
||||
nhất trí
|
||||
trí tuệ
|
||||
tuệ giác
|
||||
|
||||
quán triệt
|
||||
triệt để
|
||||
để dành
|
||||
dành dụm
|
||||
|
||||
sôi nổi
|
||||
nổi tiếng
|
||||
tiếng nói
|
||||
nói chuyện
|
||||
chuyện trò
|
||||
trò chơi
|
||||
chơi đùa
|
||||
|
||||
nở hoa
|
||||
hoa quả
|
||||
hóa học
|
||||
hóa đơn
|
||||
đơn giản
|
||||
giản dị
|
||||
dị thường
|
||||
thường xuyên
|
||||
xuyên tạc
|
||||
|
||||
trắc nghiệm
|
||||
nghiệm thu
|
||||
thu nhập
|
||||
nhập khẩu
|
||||
khẩu hiệu
|
||||
hiệu quả
|
||||
|
||||
# --- a few longer words, so syllable bonuses and the 3+ badge have cases ---
|
||||
vô tuyến điện
|
||||
công nghiệp hóa
|
||||
tổng hợp chất
|
||||
điện thoại di động
|
||||
Reference in new issue
Block a user