106 lines
5.2 KiB
Go
106 lines
5.2 KiB
Go
package membank
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/lang"
|
||
"textmachine/backend/internal/store"
|
||
"textmachine/backend/internal/text"
|
||
)
|
||
|
||
func present(t *testing.T, e *entry, output string, s lang.TargetStemmer) bool {
|
||
t.Helper()
|
||
norm := text.NormalizeTargetForm(output)
|
||
return dstFormPresent(e, []rune(norm), lang.TokenizeWords(norm), s)
|
||
}
|
||
|
||
// TestDstFormPresentAcceptsDeclension is the §3 pin: a term rendered in an oblique case is ACCEPTED against
|
||
// its nominative even when the seed listed no decl forms (the measured 142/142-null noise), while a term
|
||
// genuinely absent is still a miss. Multi-word terms match component-wise, whichever word inflected.
|
||
func TestDstFormPresentAcceptsDeclension(t *testing.T) {
|
||
s := lang.NewTargetStemmer(lang.TargetChecksFor("ru"))
|
||
single := &entry{dst: "меч"}
|
||
if !present(t, single, "он поднял свой мечом высоко", s) {
|
||
t.Fatal("an oblique case of a single-word term must be accepted (мечом ~ меч)")
|
||
}
|
||
if present(t, single, "он поднял свой клинок высоко", s) {
|
||
t.Fatal("a genuinely absent term must still be a miss")
|
||
}
|
||
multi := &entry{dst: "гора Цинмао"}
|
||
if !present(t, multi, "они стояли у горы Цинмао", s) {
|
||
t.Fatal("a multi-word term with the head inflected must be accepted (горы Цинмао ~ гора Цинмао)")
|
||
}
|
||
// Fix-pack §а: the Palladius soft-sign case («Фан Юаню»~«Фан Юань») is NOT closed here — a bare «ь»
|
||
// decl_suffix that would close it also collapses «Синь»/«Линь» onto «синий»/«линия» (a SpoilerLeaks
|
||
// false-positive), so it was rejected. The soft-sign gap is left for seed decl-enumeration; this asserts
|
||
// the current honest behaviour (an oblique soft-sign name is NOT matched) so a naive re-add is visible.
|
||
if present(t, &entry{dst: "Синь"}, "небо было синим", s) {
|
||
t.Fatal("«Синь» must NOT match «синим» — a bare «ь» decl_suffix re-introduced the collision (fix-pack §а)")
|
||
}
|
||
}
|
||
|
||
// TestDstFormPresentAcceptsSeedDeclForms is the D39.76 POSITIVE counterpart to the «Синь» negative guard
|
||
// above: the soft-sign gap is closed by DATA (seed decl.forms), not by adding «ь» to the stemmer. A name
|
||
// whose seed lists its oblique forms is accepted in an oblique case via the generic whole-word declForms
|
||
// path, while WITHOUT those forms the same case still misses (the honest §3 behaviour). This pins the case
|
||
// that previously held only by a comment (review D39.75 PARTIAL). declForms are stored normalized
|
||
// (memory.go:320 runs NormalizeTargetForm on load) — mirror that when building the entry directly.
|
||
func TestDstFormPresentAcceptsSeedDeclForms(t *testing.T) {
|
||
s := lang.NewTargetStemmer(lang.TargetChecksFor("ru"))
|
||
norm := func(fs ...string) []string {
|
||
out := make([]string, 0, len(fs))
|
||
for _, f := range fs {
|
||
out = append(out, text.NormalizeTargetForm(f))
|
||
}
|
||
return out
|
||
}
|
||
seeded := &entry{dst: "Фан Юань", declForms: norm("Фан Юаня", "Фан Юаню", "Фан Юанем", "Фан Юане")}
|
||
if !present(t, seeded, "он поклонился Фан Юаню и ушёл", s) {
|
||
t.Fatal("a soft-sign name with seed decl.forms must be accepted in an oblique case (Фан Юаню via forms[], not «ь» stemming)")
|
||
}
|
||
if present(t, &entry{dst: "Фан Юань"}, "он поклонился Фан Юаню и ушёл", s) {
|
||
t.Fatal("without seed decl.forms the soft-sign oblique case must still miss — else the gap was closed by a global suffix, not by data")
|
||
}
|
||
}
|
||
|
||
// TestDstFormPresentInertWithoutStemmer guards the generality path: a target with no decl_suffix registry
|
||
// (an inert stemmer) keeps EXACT-match behaviour, so it never falsely accepts a declined form.
|
||
func TestDstFormPresentInertWithoutStemmer(t *testing.T) {
|
||
var inert lang.TargetStemmer
|
||
e := &entry{dst: "меч"}
|
||
if present(t, e, "он поднял мечом", inert) {
|
||
t.Fatal("without a stemmer the declined form must NOT match")
|
||
}
|
||
if !present(t, e, "он поднял меч", inert) {
|
||
t.Fatal("the exact nominative must still match with no stemmer")
|
||
}
|
||
}
|
||
|
||
func TestGenderConstraintNoteNeuter(t *testing.T) {
|
||
tx := lang.InjectionTextsFor("ru")
|
||
if tx.GenderNeuter == "" {
|
||
t.Fatal("ru must ship a gender_neuter directive")
|
||
}
|
||
for _, g := range []string{"neuter", "n"} {
|
||
if got := genderConstraintNote(g, tx); got != tx.GenderNeuter {
|
||
t.Fatalf("gender %q must map to the neuter directive, got %q", g, got)
|
||
}
|
||
}
|
||
if genderConstraintNote("xyz", tx) != "" {
|
||
t.Fatal("an unknown gender must produce no directive (the row-84 silent no-op it once was)")
|
||
}
|
||
}
|
||
|
||
func TestGenderVocabViolations(t *testing.T) {
|
||
entries := []store.GlossaryEntry{
|
||
{Src: "甲", Dst: "А", Gender: "male"},
|
||
{Src: "乙", Dst: "Б", Gender: "neuter"}, // now valid
|
||
{Src: "丙", Dst: "В", Gender: ""}, // absent is fine
|
||
{Src: "丁", Dst: "Г", Gender: "Male"}, // wrong case → a silent no-op today, must flag
|
||
{Src: "戊", Dst: "Д", Gender: "мужской"}, // a typo → must flag
|
||
}
|
||
bad := GenderVocabViolations(entries)
|
||
if len(bad) != 2 {
|
||
t.Fatalf("exactly the two off-vocabulary genders must flag, got %d: %v", len(bad), bad)
|
||
}
|
||
}
|