111 lines
6.2 KiB
Go
111 lines
6.2 KiB
Go
package checks
|
||
|
||
import "testing"
|
||
|
||
// TestPack13Checkers pins the pack-13 GENERAL checkers against representative inputs (no book-specific
|
||
// word lists): each defect shape fires and a clean counterpart / golden-shape Russian output stays 0
|
||
// (precision over recall). The 时辰/千万 cases confirm the pre-existing DC1/DC2 still catch the corpus shapes.
|
||
func TestPack13Checkers(t *testing.T) {
|
||
dcc := testCheckers(t)
|
||
// Percent scale (成 = tenths): a decimal-fraction rendering fires; a correct percent is suppressed.
|
||
if n, _ := dcc.lintPercentScale("море истинной ци 六成六 …", "море истинной ци — шесть десятых и шесть сотых"); n != 1 {
|
||
t.Errorf("percent: «шесть десятых и шесть сотых» should fire, got %d", n)
|
||
}
|
||
if n, _ := dcc.lintPercentScale("六成六", "море истинной ци — шесть и шесть десятых"); n != 1 {
|
||
t.Errorf("percent: «шесть и шесть десятых» should fire, got %d", n)
|
||
}
|
||
if n, _ := dcc.lintPercentScale("六成六", "заполнено на шестьдесят шесть процентов"); n != 0 {
|
||
t.Errorf("percent: a correct «процентов» rendering must be suppressed, got %d", n)
|
||
}
|
||
if n, _ := dcc.lintPercentScale("нет числа", "шесть десятых чего-то"); n != 0 {
|
||
t.Errorf("percent: no 成 in source must not fire, got %d", n)
|
||
}
|
||
|
||
// Latin residue (#6, D39.39): a leaked Latin WORD fires whether lowercase OR capitalised — in a Russian
|
||
// target a Latin proper noun/brand is itself a leak (transliterate to Cyrillic) and «BANK»/«TM-BANK» is
|
||
// contamination. Legit Latin-kept surfaces live on the per-project allowlist, NOT on a "has a capital" heuristic.
|
||
for _, fires := range []struct {
|
||
text string
|
||
want int
|
||
}{
|
||
{"открыл их again, горестно вздохнув", 1}, // lowercase prose leak
|
||
{"остаточное слово Cultivation в тексте", 1}, // Title-case leaked common noun (was invisible pre-#6)
|
||
{"маркер BANK протёк в финал", 1}, // ALL-CAPS bank-marker fragment (was invisible pre-#6)
|
||
{"Он держал в руках iPhone и MacBook", 2}, // two Latin brand tokens (allowlist territory, below)
|
||
} {
|
||
if n, det := lintLatinResidue(fires.text, nil); n != fires.want {
|
||
t.Errorf("latin: %q should fire ×%d, got %d %v", fires.text, fires.want, n, det)
|
||
}
|
||
}
|
||
for _, clean := range []string{
|
||
"ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД 5abc35ddfb65. Судзуки шёл по коридорам.", // a body-hash id (has digits)
|
||
"Классы таланта А, Б, В и Г — от высшей к низшей.", // Cyrillic class letters (not Latin)
|
||
"Глава II, раздел XII начинается.", // Roman numerals (case-insensitive exclusion)
|
||
"Судзуки шёл в Академию.", // Cyrillic proper noun
|
||
} {
|
||
if n, det := lintLatinResidue(clean, nil); n != 0 {
|
||
t.Errorf("latin: clean text must not fire (%q): %d %v", clean, n, det)
|
||
}
|
||
}
|
||
// The per-project allowlist exempts an intentional Latin surface — lowercase OR a capitalised brand
|
||
// (checked case-insensitively), the correct home for a kept brand rather than a "has-capital" blanket skip.
|
||
if n, _ := lintLatinResidue("сказал again снова", map[string]bool{"again": true}); n != 0 {
|
||
t.Errorf("latin: an allowlisted lowercase surface must not fire")
|
||
}
|
||
if n, _ := lintLatinResidue("держал iPhone и MacBook", map[string]bool{"iphone": true, "macbook": true}); n != 0 {
|
||
t.Errorf("latin: allowlisted brands (case-insensitive) must not fire")
|
||
}
|
||
|
||
// Broken word — the general «-йть» rule only (no book-specific lists): «войть» fires, valid words do not.
|
||
if n, _ := dcc.lintBrokenWord("хотел тихонько войть и закрыть"); n != 1 {
|
||
t.Errorf("broken: «войть» (-йть) should fire")
|
||
}
|
||
for _, clean := range []string{
|
||
"он решил войти и закрыть окно", // valid войти
|
||
"глава клана Гуюэ поклонился", // valid prose (no -йть)
|
||
"впереди идёт Фан Юань", // valid впереди
|
||
} {
|
||
if n, det := dcc.lintBrokenWord(clean); n != 0 {
|
||
t.Errorf("broken: clean text must not fire (%q): %d %v", clean, n, det)
|
||
}
|
||
}
|
||
|
||
// The pre-existing DC1/DC2 still catch the corpus shapes (general Chinese units/idioms).
|
||
if n, _ := dcc.lintTimeUnits("僵持了三个时辰", "прошло три часа"); n != 1 {
|
||
t.Errorf("time-unit: 三个时辰→«три часа» should fire")
|
||
}
|
||
if n, _ := dcc.lintMagnitudeScale("千万生灵", "погубил тысячи жизней"); n != 1 {
|
||
t.Errorf("magnitude: 千万→«тысячи» should fire")
|
||
}
|
||
}
|
||
|
||
// TestMagnitudeScaleEmptyProbeIsInert pins the pack-15 empty-probe guard: a probe WORD missing from the
|
||
// pair data must leave DC2's 千万 branch inert, never spurious. strings.Contains(x, "") is true for every
|
||
// x, so an empty fire word would have flagged every chunk whose source carries 千万 — a data slip reading
|
||
// as a checker bug — and an empty veto word would have silently disabled the branch.
|
||
func TestMagnitudeScaleEmptyProbeIsInert(t *testing.T) {
|
||
full := testCheckers(t)
|
||
src, bad := "他有千万家产。", "У него тысячи домов."
|
||
if n, _ := full.lintMagnitudeScale(src, bad); n == 0 {
|
||
t.Fatalf("test premise broken: the configured probe must flag this pair (got %d)", n)
|
||
}
|
||
for _, tc := range []struct {
|
||
name string
|
||
mutus func(*Checkers)
|
||
}{
|
||
{"empty fire word", func(c *Checkers) { c.qianwanFireWord = "" }},
|
||
{"empty veto word", func(c *Checkers) { c.qianwanVetoWord = "" }},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
c := *testCheckers(t) // copy the compiled spec
|
||
tc.mutus(&c)
|
||
if n, det := c.lintMagnitudeScale(src, bad); n != 0 {
|
||
t.Errorf("an empty probe word must leave the branch INERT, got %d flags: %v", n, det)
|
||
}
|
||
// A clean chunk stays clean either way (no spurious fire on unrelated text).
|
||
if n, _ := c.lintMagnitudeScale("他走了三里。", "Он прошёл три ли."); n != 0 {
|
||
t.Errorf("clean chunk must not flag, got %d", n)
|
||
}
|
||
})
|
||
}
|
||
}
|