mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-13 16:15:22 +00:00
expvar counters for connections, rooms, games, submissions by rejection reason, eliminations, chat, joins and bot moves, served on a separate debug address so they never sit on the public mux, plus one structured word_rejected log line per refused word carrying the normalized word and its link. GET /readyz flips to 503 while draining; SIGTERM stops new rooms, waits up to NOITU_DRAIN_TIMEOUT for live games, then shuts down. GET /version and the startup log carry the build's git describe. Fuzz targets for the frame decoder, the text sanitizer and Vietnamese normalization; the last one found that composing before lowercasing could leave a non-NFC result, now recomposed after lowering. CI runs on dev as well as main, gates gofmt and golangci-lint, tracks the buf major instead of an exact pin, and dependabot watches every ecosystem. The lint findings that had been hidden by the default per-issue cap are fixed.
142 lines
4.4 KiB
Go
142 lines
4.4 KiB
Go
package vietnamese
|
||
|
||
import (
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
|
||
"golang.org/x/text/unicode/norm"
|
||
)
|
||
|
||
func TestNormalize(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
raw string
|
||
want string
|
||
syllables int
|
||
}{
|
||
{"plain two syllables", "pháp luật", "pháp luật", 2},
|
||
{"uppercase", "PHÁP LUẬT", "pháp luật", 2},
|
||
{"mixed case", "Pháp Luật", "pháp luật", 2},
|
||
{"leading and trailing space", " pháp luật ", "pháp luật", 2},
|
||
{"repeated spaces", "pháp luật", "pháp luật", 2},
|
||
{"tab separator", "pháp\tluật", "pháp luật", 2},
|
||
{"non-breaking space", "pháp luật", "pháp luật", 2},
|
||
{"newline separator", "pháp\nluật", "pháp luật", 2},
|
||
{"three syllables", "vô tuyến điện", "vô tuyến điện", 3},
|
||
{"four syllables", "công nghiệp hóa hiện", "công nghiệp hóa hiện", 4},
|
||
{"single syllable passes through", "pháp", "pháp", 1},
|
||
}
|
||
|
||
for _, tc := range tests {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
word, syllables, err := Normalize(tc.raw)
|
||
if err != nil {
|
||
t.Fatalf("Normalize(%q) returned error: %v", tc.raw, err)
|
||
}
|
||
if word != tc.want {
|
||
t.Errorf("word = %q, want %q", word, tc.want)
|
||
}
|
||
if len(syllables) != tc.syllables {
|
||
t.Errorf("got %d syllables, want %d", len(syllables), tc.syllables)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// A word typed on one keyboard can arrive decomposed and on another composed.
|
||
// Both must normalize to the same string, or database lookups fail for reasons
|
||
// invisible to the player.
|
||
func TestNormalizeUnicodeEquivalence(t *testing.T) {
|
||
composed := "ngôn ngữ" // ữ as a single codepoint
|
||
decomposed := "ngôn ngữ" // o+circumflex, u+horn+tilde
|
||
|
||
gotComposed, _, err := Normalize(composed)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
gotDecomposed, _, err := Normalize(decomposed)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
if gotComposed != gotDecomposed {
|
||
t.Errorf("composed %q and decomposed %q normalized differently: %q vs %q",
|
||
composed, decomposed, gotComposed, gotDecomposed)
|
||
}
|
||
}
|
||
|
||
func TestNormalizeEmpty(t *testing.T) {
|
||
for _, raw := range []string{"", " ", "\t\n", " "} {
|
||
if _, _, err := Normalize(raw); !errors.Is(err, ErrEmpty) {
|
||
t.Errorf("Normalize(%q) error = %v, want ErrEmpty", raw, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// FuzzNormalize guards the one function two processes have to agree on
|
||
// byte-for-byte: the corpus builder normalizes with it at build time, the
|
||
// server normalizes with it on every submission, and a divergence between
|
||
// them would silently unmatch a word the dictionary actually has. The
|
||
// invariants are exactly what the package doc promises — NFC, lowercase,
|
||
// single-spaced — plus no panic on arbitrary input, and that the returned
|
||
// word and syllables always agree with each other.
|
||
func FuzzNormalize(f *testing.F) {
|
||
seeds := []string{
|
||
"pháp luật", "PHÁP LUẬT", "Pháp Luật", " pháp luật ", "pháp luật",
|
||
"pháp\tluật", "pháp luật", "pháp\nluật", "vô tuyến điện",
|
||
"công nghiệp hóa hiện", "pháp", "", " ", "\t\n", " ",
|
||
"ngôn ngữ", "ngôn ngữ",
|
||
}
|
||
for _, s := range seeds {
|
||
f.Add(s)
|
||
}
|
||
|
||
f.Fuzz(func(t *testing.T, raw string) {
|
||
word, syllables, err := Normalize(raw)
|
||
if err != nil {
|
||
if !errors.Is(err, ErrEmpty) {
|
||
t.Fatalf("Normalize(%q) returned an unexpected error: %v", raw, err)
|
||
}
|
||
if word != "" || syllables != nil {
|
||
t.Fatalf("Normalize(%q) returned ErrEmpty but non-empty output: %q %v", raw, word, syllables)
|
||
}
|
||
return
|
||
}
|
||
|
||
if !norm.NFC.IsNormalString(word) {
|
||
t.Fatalf("Normalize(%q) = %q is not NFC", raw, word)
|
||
}
|
||
if strings.ToLower(word) != word {
|
||
t.Fatalf("Normalize(%q) = %q is not fully lowercase", raw, word)
|
||
}
|
||
if strings.Join(strings.Fields(word), " ") != word {
|
||
t.Fatalf("Normalize(%q) = %q is not single-spaced and trimmed", raw, word)
|
||
}
|
||
if len(syllables) == 0 {
|
||
t.Fatalf("Normalize(%q) returned no error but no syllables", raw)
|
||
}
|
||
if strings.Join(syllables, " ") != word {
|
||
t.Fatalf("Normalize(%q): word %q does not match its own syllables %v", raw, word, syllables)
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestHasEnoughSyllables(t *testing.T) {
|
||
tests := []struct {
|
||
syllables []string
|
||
want bool
|
||
}{
|
||
{[]string{"pháp"}, false},
|
||
{[]string{"pháp", "luật"}, true},
|
||
{[]string{"vô", "tuyến", "điện"}, true},
|
||
{nil, false},
|
||
}
|
||
|
||
for _, tc := range tests {
|
||
if got := HasEnoughSyllables(tc.syllables); got != tc.want {
|
||
t.Errorf("HasEnoughSyllables(%v) = %v, want %v", tc.syllables, got, tc.want)
|
||
}
|
||
}
|
||
}
|