156 lines
6.2 KiB
Go
156 lines
6.2 KiB
Go
package lang
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// TestEmbeddedVersionMutation is the П0 acceptance test (D39.60 §6.1, "bytes ARE version" for the embed
|
||
// plane): a byte edit of an embedded data file MUST move EmbeddedVersion — so, once it is folded into the
|
||
// snapshot, an edit is a loud --resnapshot, never a silent verdict/wire change. It also pins the format and
|
||
// reproducibility (the same bytes always hash the same).
|
||
func TestEmbeddedVersionMutation(t *testing.T) {
|
||
got := EmbeddedVersion()
|
||
if !strings.HasPrefix(got, embedAlgoVersion+"-") {
|
||
t.Fatalf("EmbeddedVersion() = %q, want %s-<hash>", got, embedAlgoVersion)
|
||
}
|
||
|
||
files := embeddedDataFiles()
|
||
if len(files) == 0 {
|
||
t.Fatal("no embedded data files")
|
||
}
|
||
if base := hashEmbeddedFiles(files); base != got {
|
||
t.Fatalf("EmbeddedVersion not reproducible from embeddedDataFiles(): %s != %s", base, got)
|
||
}
|
||
|
||
// Mutate ONE byte of a verdict-bearing file (target-ru.txt drives the sanitizer) and a wire-bearing file
|
||
// (injection.txt rides the request) — each must diverge the hash on its own.
|
||
for _, target := range []string{"target-ru.txt", "injection.txt"} {
|
||
mutated := make([]embeddedFile, len(files))
|
||
copy(mutated, files)
|
||
var found bool
|
||
for i := range mutated {
|
||
if mutated[i].name == target {
|
||
b := append([]byte(nil), mutated[i].data...)
|
||
b[len(b)/2] ^= 0x01 // flip a content byte
|
||
mutated[i].data = b
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("%s is not embedded", target)
|
||
}
|
||
if mut := hashEmbeddedFiles(mutated); mut == got {
|
||
t.Errorf("a byte edit of %s did NOT move EmbeddedVersion (%s) — the embed plane would drift silently", target, mut)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestTheInjectionKeysAreOneVocabulary pins the property the key table exists for: the keys the PARSER
|
||
// accepts and the keys a guard may REQUIRE are the same set, read from one place. A guard naming its own
|
||
// literals would pass forever on a key the file stopped carrying, and the run it was supposed to refuse —
|
||
// a bank its target cannot state — is silent by construction.
|
||
func TestTheInjectionKeysAreOneVocabulary(t *testing.T) {
|
||
// Every key the parser accepts must be answerable by Authored. A key that parses into a field nothing
|
||
// can read back is a row a guard cannot see.
|
||
for key := range injectionFields {
|
||
parsed, err := parseInjection([]byte("xx\t" + key + "\tзначение"))
|
||
if err != nil {
|
||
t.Fatalf("the parser accepts %q but this file could not be parsed: %v", key, err)
|
||
}
|
||
if !parsed["xx"].Authored(key) {
|
||
t.Errorf("key %q parses into a field Authored cannot read back — a guard asking for it would never see the row", key)
|
||
}
|
||
if parsed["xx"].Authored("no_such_key") {
|
||
t.Errorf("an unknown key must never read as authored (%q)", key)
|
||
}
|
||
}
|
||
if _, err := parseInjection([]byte("xx\tno_such_key\tзначение")); err == nil {
|
||
t.Error("an unknown injection key must fail the parse loudly — a typo that parses is a row nobody renders")
|
||
}
|
||
}
|
||
|
||
// TestAPartialInjectionSetIsVisibleToTheGuard is the predicate half of the write-path refusal (pipeline:
|
||
// checkInjectionTexts). It lives here and not there because the EMBEDDED data carries exactly one target
|
||
// (ru), and giving it a second, synthetic one to test against would re-mint the unit id of every book
|
||
// alive: injection.txt is folded byte-for-byte into EmbeddedVersion. So the wiring is pinned end-to-end on
|
||
// a target with NO rows, and the half-authored shapes are pinned here, on bytes.
|
||
func TestAPartialInjectionSetIsVisibleToTheGuard(t *testing.T) {
|
||
required := []string{InjectionKeyGlossaryHeader, InjectionKeyEditorHeader}
|
||
for _, tc := range []struct {
|
||
name string
|
||
rows string
|
||
missing []string
|
||
gaps []string
|
||
}{
|
||
{
|
||
name: "a target with no rows at all is missing both headers",
|
||
rows: "",
|
||
missing: []string{InjectionKeyGlossaryHeader, InjectionKeyEditorHeader},
|
||
},
|
||
{
|
||
// The half-step the HasData gate cannot see: it reads the glossary header alone, so this target
|
||
// passes it and hands the editor a law list with an EMPTY line where its instruction belongs.
|
||
name: "a glossary header without an editor header",
|
||
rows: "xx\tglossary_header\tГЛОССАРИЙ:\n",
|
||
missing: []string{InjectionKeyEditorHeader},
|
||
},
|
||
{
|
||
name: "both headers and no gender directives is a gender-free target, not a gap",
|
||
rows: "xx\tglossary_header\tГЛОССАРИЙ:\nxx\teditor_header\tКАНОН:\n",
|
||
},
|
||
{
|
||
name: "two gender directives out of three is a gap, and it names the missing one",
|
||
rows: "xx\tglossary_header\tГЛОССАРИЙ:\nxx\teditor_header\tКАНОН:\n" +
|
||
"xx\tgender_male\t (муж.)\nxx\tgender_female\t (жен.)\n",
|
||
gaps: []string{InjectionKeyGenderHidden},
|
||
},
|
||
{
|
||
// gender_neuter is documented optional: not every target language has a neuter to name.
|
||
name: "all three directives and no neuter is complete",
|
||
rows: "xx\tglossary_header\tГЛОССАРИЙ:\nxx\teditor_header\tКАНОН:\n" +
|
||
"xx\tgender_male\t (муж.)\nxx\tgender_female\t (жен.)\nxx\tgender_hidden\t (скрыт)\n",
|
||
},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
parsed, err := parseInjection([]byte(tc.rows))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
tx := InjectionTexts{}
|
||
if p, ok := parsed["xx"]; ok {
|
||
tx = *p
|
||
}
|
||
if got := tx.MissingKeys(required); !equalStrings(got, tc.missing) {
|
||
t.Errorf("MissingKeys = %v, want %v", got, tc.missing)
|
||
}
|
||
if got := tx.GenderKeyGaps(); !equalStrings(got, tc.gaps) {
|
||
t.Errorf("GenderKeyGaps = %v, want %v", got, tc.gaps)
|
||
}
|
||
})
|
||
}
|
||
|
||
// ⛔ AND THE SHIPPING TARGET, because every assertion above is about synthetic bytes. The one target
|
||
// this engine ships must author everything the guard requires AND the whole gender set — otherwise the
|
||
// refusal and the warning above would fire on the only pair in production.
|
||
ru := InjectionTextsFor("ru")
|
||
if m := ru.MissingKeys(required); len(m) > 0 {
|
||
t.Errorf("the shipping target `ru` is missing injection rows the write path requires: %v", m)
|
||
}
|
||
if g := ru.GenderKeyGaps(); len(g) > 0 {
|
||
t.Errorf("the shipping target `ru` authors a PARTIAL gender set (%v) — the warning would fire on every production run", g)
|
||
}
|
||
}
|
||
|
||
func equalStrings(a, b []string) bool {
|
||
if len(a) != len(b) {
|
||
return false
|
||
}
|
||
for i := range a {
|
||
if a[i] != b[i] {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|