package lang import ( "strings" "testing" ) // TestBankPlaneIsOutsideTheWaveFold is the load-bearing property of the whole plane, and the reason it is a // separate embed rather than another row in data/script-series.txt: editing family morphology must NOT move // the wave snapshot. EmbeddedVersion folds into every book's snapshot, so a row added there would re-bill a // whole draft wave for a knob that only decides which candidates share a terminologist CALL — the axis // config.TerminologyGate is deliberately kept off the snapshot for. func TestBankPlaneIsOutsideTheWaveFold(t *testing.T) { for _, f := range embeddedDataFiles() { if strings.Contains(f.name, "family") { t.Fatalf("the bank plane's %s reached the WAVE-folded embed set — editing it would re-snapshot every book", f.name) } } // Both versions exist and are distinct namespaces, so a log line naming one can never be read as the other. if v := BankDataVersion(); !strings.HasPrefix(v, bankDataAlgoVersion+"-") { t.Fatalf("BankDataVersion must be tagged with its own recipe, got %q", v) } if strings.HasPrefix(BankDataVersion(), embedAlgoVersion) { t.Fatal("the two data planes must not share a version namespace") } } // TestFamilyMorphologyIsData drives the parser with a synthetic table: a script's family rules — including // the name/realia asymmetry and the head side — are DATA, so a pair that is not in the repo declares its own // and needs no Go edit. The script here is a REGISTERED one that ships no family rows of its own — the first // version of this test declared «devanagari», which resolves to no Unicode range and was therefore dropped // in silence, so the flagship generality test proved nothing at all. func TestFamilyMorphologyIsData(t *testing.T) { got, err := parseFamilyMorphology([]byte(strings.Join([]string{ "# a source whose NAMES are head-final and whose realia lead with the root", "family_affix\thangul\tname\tsuffix\t3", "family_affix\thangul\tterm\tprefix\t2", "family_min_members\thangul\t4", "family_containment_runes\thangul\t3", }, "\n"))) if err != nil { t.Fatal(err) } sf := got["hangul"] if sf == nil { t.Fatalf("the declared script must be present: %v", got) } if a := sf.affix["name"]; !a.Suffix || a.MinRunes != 3 { t.Fatalf("a head-final name rule is one data row, got %+v", a) } if a := sf.affix["term"]; a.Suffix || a.MinRunes != 2 { t.Fatalf("the asymmetry is per-type data, got %+v", a) } if sf.minMembers != 4 || sf.containmentRunes != 3 || sf.maxMembers != 0 { t.Fatalf("bounds come from the rows, absent ones stay unset: %+v", sf) } } // TestFamilyMorphologyRefusesCorruptRows: a malformed row is a corrupt asset, and the failure mode it would // otherwise produce — a channel that parses fine and silently forms nothing — is the exact drift the pack // loader already refuses for the Palladius tables. func TestFamilyMorphologyRefusesCorruptRows(t *testing.T) { for _, bad := range []string{ "family_affix\than\tname\tmiddle\t2", // no such side "family_affix\than\tname\tprefix\t0", // a zero-rune root matches everything "family_affix\than\tname\tprefix", // truncated row "family_minmembers\than\t2", // typo'd key "family_min_members\than\tlots", // non-integer // A typo'd SCRIPT resolves to no range, so every row naming it is silently ignored and the pair that // asked for the channel gets an inert one. "family_affix\thann\tname\tprefix\t2\nfamily_min_members\thann\t2", // A HALF table: rules with no quorum, or a quorum with nothing to form a family from. Both parse and // then do nothing, which is the failure this pack declared intolerable everywhere else. "family_affix\than\tname\tprefix\t2", "family_min_members\than\t2", } { if _, err := parseFamilyMorphology([]byte(bad)); err == nil { t.Fatalf("a corrupt row must fail loud: %q", bad) } } } // TestFamilyMorphologyForZhIsLive pins the shipped table through the resolver a book actually calls: zh is // written in a dense script that declares rules, an alphabetic source is inert, and the ja case shows the // union — kana declare nothing, so the language inherits han's rules rather than losing them. func TestFamilyMorphologyForZhIsLive(t *testing.T) { zh := FamilyMorphology("zh") if !zh.Enabled() { t.Fatal("zh is the measured pair and must have a live family channel") } if a := zh.Affix["name"]; a.Suffix || a.MinRunes < 2 { t.Fatalf("a han name family leads with the clan morpheme: %+v", a) } if a := zh.Affix["term"]; !a.Suffix || a.MinRunes < 2 { t.Fatalf("a han realia family ends with the generic head: %+v", a) } if zh.MinMembers < 2 || zh.MaxMembers <= 0 || zh.ContainmentRunes < 2 { t.Fatalf("the bounds must all be declared: %+v", zh) } if en := FamilyMorphology("en"); en.Enabled() { t.Fatalf("an alphabetic source forms no morpheme families: %+v", en) } if ja := FamilyMorphology("ja"); !ja.Enabled() || ja.Affix["term"] != zh.Affix["term"] { t.Fatalf("a language written in han inherits han's rules through the union: %+v", ja) } }