textmachine/backend/internal/membank/memdecl_test.go

106 lines
5.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}