145 lines
5.4 KiB
Go
145 lines
5.4 KiB
Go
package fakes
|
|
|
|
import "testing"
|
|
|
|
// TestRootReferenceAcrossFolders is the headline case: a category in one folder
|
|
// pulls a value from another via a {..path} reference resolved from the data root.
|
|
func TestRootReferenceAcrossFolders(t *testing.T) {
|
|
dir := writeData(t, map[string]string{
|
|
"en_US/person": `["Pat Smith"]`,
|
|
"sv_SE/greeting": `{"format":"Hej, {..en_US.person}!"}`,
|
|
})
|
|
f := newFakes(t, dir, WithSeed(1))
|
|
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
|
|
t.Fatalf("greeting = %q, want \"Hej, Pat Smith!\"", got)
|
|
}
|
|
}
|
|
|
|
// TestReferenceIntoAField reaches a field inside a referenced category, crossing
|
|
// a single-variant choice and then a template field (..who.last).
|
|
func TestReferenceIntoAField(t *testing.T) {
|
|
dir := writeData(t, map[string]string{
|
|
"who": `[{"format":"{first} {last}","first":["Ada"],"last":["Byron"]}]`,
|
|
"card": `{"format":"signed {..who.last}"}`,
|
|
})
|
|
f := newFakes(t, dir, WithSeed(1))
|
|
if got := fake(t, f, "card"); got != "signed Byron" {
|
|
t.Fatalf("card = %q, want \"signed Byron\"", got)
|
|
}
|
|
}
|
|
|
|
// TestReferenceInAlternation lets a reference stand as one arm of a {a|..b}
|
|
// alternation, so a field and a cross-file value share one slot.
|
|
func TestReferenceInAlternation(t *testing.T) {
|
|
dir := writeData(t, map[string]string{
|
|
"far": `["X"]`,
|
|
"near": `{"format":"{here|..far}","here":["H"]}`,
|
|
})
|
|
f := newFakes(t, dir, WithSeed(2))
|
|
seen := map[string]bool{}
|
|
for i := 0; i < 100; i++ {
|
|
seen[fake(t, f, "near")] = true
|
|
}
|
|
if !seen["H"] || !seen["X"] || len(seen) != 2 {
|
|
t.Fatalf("alternation produced %v, want both H and X", seen)
|
|
}
|
|
}
|
|
|
|
// TestReferenceCombinesLoadedPaths is the point of references over the merge
|
|
// model: data layered from two dirs can point at each other through the root.
|
|
func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
|
a := writeData(t, map[string]string{"en_US/word": `["river"]`})
|
|
b := writeData(t, map[string]string{"mine/slug": `{"format":"the-{..en_US.word}"}`})
|
|
f := newFakesN(t, []string{a, b}, WithSeed(1))
|
|
if got := fake(t, f, "mine.slug"); got != "the-river" {
|
|
t.Fatalf("slug = %q, want the-river", got)
|
|
}
|
|
}
|
|
|
|
// TestReferenceChain follows a reference to a node that is itself a reference, so
|
|
// linking order cannot matter.
|
|
func TestReferenceChain(t *testing.T) {
|
|
dir := writeData(t, map[string]string{
|
|
"a": `{"format":"{..b}"}`,
|
|
"b": `{"format":"{..c}"}`,
|
|
"c": `["deep"]`,
|
|
})
|
|
f := newFakes(t, dir, WithSeed(1))
|
|
if got := fake(t, f, "a"); got != "deep" {
|
|
t.Fatalf("a = %q, want deep", got)
|
|
}
|
|
}
|
|
|
|
// TestReferenceErrors lists the references New must reject up front, so a bad path
|
|
// fails at load, never at a random render.
|
|
func TestReferenceErrors(t *testing.T) {
|
|
cases := map[string]map[string]string{
|
|
"missing target": {"card": `{"format":"{..nope.gone}"}`},
|
|
"folder target": {"en_US/word": `["w"]`, "card": `{"format":"{..en_US}"}`},
|
|
"multi-variant on the path": {
|
|
"who": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"]}]`,
|
|
"card": `{"format":"{..who.f}"}`,
|
|
},
|
|
"empty reference path": {"card": `{"format":"{..}"}`},
|
|
// A reference that leads back to its own value never terminates at render,
|
|
// so New must reject the cycle up front (direct, mutual, or chained).
|
|
"direct cycle": {"a": `{"format":"x{..a}"}`},
|
|
"mutual cycle": {"a": `{"format":"{..b}"}`, "b": `{"format":"{..a}"}`},
|
|
"chain cycle": {"a": `{"format":"{..b}"}`, "b": `{"format":"{..c}"}`, "c": `{"format":"{..a}"}`},
|
|
// calc renders its operands, so a cycle through one must be caught too.
|
|
"calc operand cycle": {"x": `{"format":"{calc(y)}","y":[{"format":"{..x}"}]}`},
|
|
// A field its parent's format never renders is still reachable by dot path,
|
|
// so a cycle hiding in one must fail at New rather than at render.
|
|
"cycle in an unrendered field": {"cat": `{"format":"hi","x":{"format":"{..cat.x}"}}`},
|
|
"mutual cycle between unrendered fields": {
|
|
"cat": `{"format":"hi","x":{"format":"{..cat.y}"},"y":{"format":"{..cat.x}"}}`,
|
|
},
|
|
"cycle in an unrendered field of a choice arm": {
|
|
"cat": `[{"format":"hi","x":{"format":"{..cat.x}"}}]`,
|
|
},
|
|
}
|
|
for name, files := range cases {
|
|
if _, err := New([]string{writeData(t, files)}); err == nil {
|
|
t.Errorf("%s: New = nil error, want a reference error", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestReferenceFromUnrenderedFieldTerminates guards the cycle check against
|
|
// over-rejecting: a field the format never renders may point back at its own
|
|
// category, which terminates, and stays renderable by path.
|
|
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
|
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":{"format":"see {..cat}"}}`})
|
|
f := newFakes(t, dir, WithSeed(1))
|
|
if got := fake(t, f, "cat"); got != "hi" {
|
|
t.Fatalf("cat = %q, want hi", got)
|
|
}
|
|
if got := fake(t, f, "cat.x"); got != "see hi" {
|
|
t.Fatalf("cat.x = %q, want \"see hi\"", got)
|
|
}
|
|
}
|
|
|
|
// TestNewErrorIsDeterministic pins one message per broken data set: map iteration
|
|
// order must not decide which of several problems the user is told about.
|
|
func TestNewErrorIsDeterministic(t *testing.T) {
|
|
dir := writeData(t, map[string]string{
|
|
"a": `{"format":"{..nope.one}"}`,
|
|
"b": `{"format":"{..nope.two}"}`,
|
|
"c": `{"format":"{..nope.three}"}`,
|
|
})
|
|
var first string
|
|
for i := 0; i < 50; i++ {
|
|
_, err := New([]string{dir})
|
|
if err == nil {
|
|
t.Fatal("New = nil error, want a reference error")
|
|
}
|
|
if i == 0 {
|
|
first = err.Error()
|
|
continue
|
|
}
|
|
if err.Error() != first {
|
|
t.Fatalf("New error varies between runs:\n %s\n %s", first, err.Error())
|
|
}
|
|
}
|
|
}
|