Draw groups: one reference draw per render, a drawGroup option, and a record's fences at load #16
+74
-33
@@ -91,17 +91,28 @@ func TestReferenceNamingABoundLevelIsRejected(t *testing.T) {
|
|||||||
// A reference can name a bound level from the data root, which renders it
|
// A reference can name a bound level from the data root, which renders it
|
||||||
// afresh beside the path that reads its held draw — the same overlap by
|
// afresh beside the path that reads its held draw — the same overlap by
|
||||||
// another spelling.
|
// another spelling.
|
||||||
rejected := map[string]string{
|
// A category never references itself, so each reference back into cat sits in a
|
||||||
"reference names the head": `{"format":"{p.first}|{/cat.p}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
// second category cat renders.
|
||||||
"reference names the leaf": `{"format":"{p.addr}|{/cat.p.addr}","p":{"format":"x","addr":["A","B","C","D"]}}`,
|
rejected := map[string]map[string]string{
|
||||||
|
"reference names the head": {
|
||||||
|
"cat": `{"format":"{p.first}|{/hop}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
|
"hop": `"{/cat.p}"`,
|
||||||
|
},
|
||||||
|
"reference names the leaf": {
|
||||||
|
"cat": `{"format":"{p.addr}|{/hop}","p":{"format":"x","addr":["A","B","C","D"]}}`,
|
||||||
|
"hop": `"{/cat.p.addr}"`,
|
||||||
|
},
|
||||||
// The reference need not sit in the format that binds: any field it renders
|
// The reference need not sit in the format that binds: any field it renders
|
||||||
// reaches the level just the same, however deep.
|
// reaches the level just the same, however deep.
|
||||||
"reference from a sibling field": `{"format":"{p.first}|{inner}","p":[` +
|
"reference from a sibling field": {
|
||||||
|
"cat": `{"format":"{p.first}|{inner}","p":[` +
|
||||||
`{"format":"{first}-{last}","first":"A","last":"1"},{"format":"{first}-{last}","first":"B","last":"2"}],` +
|
`{"format":"{first}-{last}","first":"A","last":"1"},{"format":"{first}-{last}","first":"B","last":"2"}],` +
|
||||||
`"inner":"{/cat.p}"}`,
|
`"inner":"{/hop}"}`,
|
||||||
|
"hop": `"{/cat.p}"`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, files := range rejected {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, files)))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
||||||
t.Errorf("%s: New = %v, want the reference rejected as an overlap", name, err)
|
t.Errorf("%s: New = %v, want the reference rejected as an overlap", name, err)
|
||||||
}
|
}
|
||||||
@@ -120,7 +131,8 @@ func TestCycleReachedOnlyByAPathTokenIsRejected(t *testing.T) {
|
|||||||
// cycle walk has to follow it there. A head whose own format names nothing
|
// cycle walk has to follow it there. A head whose own format names nothing
|
||||||
// would otherwise hide the cycle until render, where it is fatal.
|
// would otherwise hide the cycle until render, where it is fatal.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"a": `{"format":"{p.x}","p":{"format":"static","x":"{/a}"}}`,
|
"a": `{"format":"{p.x}","p":{"format":"static","x":"{/b}"}}`,
|
||||||
|
"b": `"{/a}"`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||||
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
||||||
@@ -131,8 +143,9 @@ func TestALevelRenderedOnlyByAPathTokenIsHeld(t *testing.T) {
|
|||||||
// {p.a} renders q, so it is a route to the level {q.x} holds — even though p's
|
// {p.a} renders q, so it is a route to the level {q.x} holds — even though p's
|
||||||
// own format names nothing.
|
// own format names nothing.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"thing": `{"format":"{p.a} {q.x}","p":{"format":"static","a":"{/thing.q}"},` +
|
"thing": `{"format":"{p.a} {q.x}","p":{"format":"static","a":"{/hop}"},` +
|
||||||
`"q":{"format":"{x}","x":["1","2"]}}`,
|
`"q":{"format":"{x}","x":["1","2"]}}`,
|
||||||
|
"hop": `"{/thing.q}"`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
||||||
t.Fatalf("New = %v, want the second route to q rejected", err)
|
t.Fatalf("New = %v, want the second route to q rejected", err)
|
||||||
@@ -149,16 +162,20 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
files map[string]string
|
files map[string]string
|
||||||
want string // the route the error names, spelled as the author wrote it
|
want string // the route the error names, spelled as the author wrote it
|
||||||
}{
|
}{
|
||||||
|
// A category never references itself, so each route back into cat sits in a
|
||||||
|
// second category cat renders.
|
||||||
"a reference beside the operand": {
|
"a reference beside the operand": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{/cat.net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
"cat": `{"format":"{/hop} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
||||||
|
"hop": `"{/cat.net}"`,
|
||||||
},
|
},
|
||||||
`{/cat.net} renders "net"`,
|
`{/hop} renders "net"`,
|
||||||
},
|
},
|
||||||
"a reference one level down": {
|
"a reference one level down": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
"cat": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
||||||
`"q":"{/cat.net}"}`,
|
`"q":"{/hop}"}`,
|
||||||
|
"hop": `"{/cat.net}"`,
|
||||||
},
|
},
|
||||||
`{q} renders "net"`,
|
`{q} renders "net"`,
|
||||||
},
|
},
|
||||||
@@ -167,7 +184,8 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a reference to an operand wrapped in a choice": {
|
"a reference to an operand wrapped in a choice": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(n * 2, 2)} {q}","n":{"format":"{v}","v":["1","2"]},` +
|
"cat": `{"format":"{calc(n * 2, 2)} {q}","n":{"format":"{v}","v":["1","2"]},` +
|
||||||
`"q":"{/cat.n}"}`,
|
`"q":"{/hop}"}`,
|
||||||
|
"hop": `"{/cat.n}"`,
|
||||||
},
|
},
|
||||||
`{q} renders "n"`,
|
`{q} renders "n"`,
|
||||||
},
|
},
|
||||||
@@ -176,7 +194,8 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"an operand reaching another operand": {
|
"an operand reaching another operand": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(a + b, 0)}","a":{"format":"{x}","x":["1","2"]},` +
|
"cat": `{"format":"{calc(a + b, 0)}","a":{"format":"{x}","x":["1","2"]},` +
|
||||||
`"b":"{/cat.a}"}`,
|
`"b":"{/hop}"}`,
|
||||||
|
"hop": `"{/cat.a}"`,
|
||||||
},
|
},
|
||||||
`calc operand "b" renders "a"`,
|
`calc operand "b" renders "a"`,
|
||||||
},
|
},
|
||||||
@@ -185,15 +204,17 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
// rejected, so the reference spelling has to be.
|
// rejected, so the reference spelling has to be.
|
||||||
"a reference into the operand": {
|
"a reference into the operand": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(net * 2, 2)}|{/cat.net.v}",` +
|
"cat": `{"format":"{calc(net * 2, 2)}|{/hop}",` +
|
||||||
`"net":{"format":"{v}","v":["10.00","20.00"]}}`,
|
`"net":{"format":"{v}","v":["10.00","20.00"]}}`,
|
||||||
|
"hop": `"{/cat.net.v}"`,
|
||||||
},
|
},
|
||||||
`{/cat.net.v} renders "net"`,
|
`{/hop} renders "net"`,
|
||||||
},
|
},
|
||||||
"a reference into the operand one level down": {
|
"a reference into the operand one level down": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(net * 2, 2)}|{q}","net":{"format":"{v}","v":["10.00","20.00"]},` +
|
"cat": `{"format":"{calc(net * 2, 2)}|{q}","net":{"format":"{v}","v":["10.00","20.00"]},` +
|
||||||
`"q":"{/cat.net.v}"}`,
|
`"q":"{/hop}"}`,
|
||||||
|
"hop": `"{/cat.net.v}"`,
|
||||||
},
|
},
|
||||||
`{q} renders "net"`,
|
`{q} renders "net"`,
|
||||||
},
|
},
|
||||||
@@ -203,7 +224,8 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a violation on the second of two held heads": {
|
"a violation on the second of two held heads": {
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"cat": `{"format":"{calc(a + b, 0)} {w}","a":{"format":"{x}","x":["1","2"]},` +
|
"cat": `{"format":"{calc(a + b, 0)} {w}","a":{"format":"{x}","x":["1","2"]},` +
|
||||||
`"b":{"format":"{y}","y":["3","4"]},"w":"{/cat.b}"}`,
|
`"b":{"format":"{y}","y":["3","4"]},"w":"{/hop}"}`,
|
||||||
|
"hop": `"{/cat.b}"`,
|
||||||
},
|
},
|
||||||
`{w} renders "b"`,
|
`{w} renders "b"`,
|
||||||
},
|
},
|
||||||
@@ -230,7 +252,8 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"a reference to a sibling the operand never renders": {
|
"a reference to a sibling the operand never renders": {
|
||||||
"cat": `{"format":"{calc(net * 2, 2)} {unit}",` +
|
"cat": `{"format":"{calc(net * 2, 2)} {unit}",` +
|
||||||
`"net":{"format":"{v}","v":["1","2"],"spare":["kg","lb"]},` +
|
`"net":{"format":"{v}","v":["1","2"],"spare":["kg","lb"]},` +
|
||||||
`"unit":"{/cat.net.spare}"}`,
|
`"unit":"{/hop}"}`,
|
||||||
|
"hop": `"{/cat.net.spare}"`,
|
||||||
},
|
},
|
||||||
// Two operands drawing from one source are two names, so two draws: each is
|
// Two operands drawing from one source are two names, so two draws: each is
|
||||||
// held under its own name and shown once, and neither can disagree.
|
// held under its own name and shown once, and neither can disagree.
|
||||||
@@ -247,7 +270,8 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
},
|
},
|
||||||
// A fixed string cannot disagree with itself, so it needs no fence.
|
// A fixed string cannot disagree with itself, so it needs no fence.
|
||||||
"a literal operand named twice": {
|
"a literal operand named twice": {
|
||||||
"cat": `{"format":"{calc(n * 2, 0)} {/cat.n}","n":"5"}`,
|
"cat": `{"format":"{calc(n * 2, 0)} {/hop}","n":"5"}`,
|
||||||
|
"hop": `"{/cat.n}"`,
|
||||||
},
|
},
|
||||||
// One node reached twice while walking the operand: the walk must not
|
// One node reached twice while walking the operand: the walk must not
|
||||||
// revisit it, and the repeat is not a second route to anything.
|
// revisit it, and the repeat is not a second route to anything.
|
||||||
@@ -267,19 +291,28 @@ func TestAPathReachesEveryVariantItMightDraw(t *testing.T) {
|
|||||||
// all of them. Reaching only the first leaves whatever hides in a later
|
// all of them. Reaching only the first leaves whatever hides in a later
|
||||||
// variant to be found at render — a cycle there is fatal, and a second route
|
// variant to be found at render — a cycle there is fatal, and a second route
|
||||||
// to a held level disagrees silently.
|
// to a held level disagrees silently.
|
||||||
rejected := map[string]struct{ file, want string }{
|
rejected := map[string]struct {
|
||||||
|
files map[string]string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
"a cycle in a later variant": {
|
"a cycle in a later variant": {
|
||||||
`{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/cat}"}]}`,
|
map[string]string{
|
||||||
|
"cat": `{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/hop}"}]}`,
|
||||||
|
"hop": `"{/cat}"`,
|
||||||
|
},
|
||||||
"reference cycle",
|
"reference cycle",
|
||||||
},
|
},
|
||||||
"a second route in a later variant": {
|
"a second route in a later variant": {
|
||||||
`{"format":"{p.x} {q.y}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/cat.q}"}],` +
|
map[string]string{
|
||||||
|
"cat": `{"format":"{p.x} {q.y}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/hop}"}],` +
|
||||||
`"q":{"format":"{y}","y":["1","2"]}}`,
|
`"q":{"format":"{y}","y":["1","2"]}}`,
|
||||||
|
"hop": `"{/cat.q}"`,
|
||||||
|
},
|
||||||
"reads a path into",
|
"reads a path into",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for name, c := range rejected {
|
for name, c := range rejected {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": c.file})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files)))
|
||||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||||
t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want)
|
t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want)
|
||||||
}
|
}
|
||||||
@@ -289,15 +322,22 @@ func TestAPathReachesEveryVariantItMightDraw(t *testing.T) {
|
|||||||
func TestALevelAPathNeverRendersIsAccepted(t *testing.T) {
|
func TestALevelAPathNeverRendersIsAccepted(t *testing.T) {
|
||||||
// A path token does not expand its head's format, so a reference sitting in
|
// A path token does not expand its head's format, so a reference sitting in
|
||||||
// that format is not a second route to anything: it is never rendered by the
|
// that format is not a second route to anything: it is never rendered by the
|
||||||
// path at all. Both orders must load.
|
// path at all. Both orders must load. p and q sit one level down, so they are
|
||||||
accepted := map[string]string{
|
// one column of thing rather than two that would read one another.
|
||||||
"reference in the head's own format": `{"format":"{p.first} {q.a}",` +
|
accepted := map[string]map[string]string{
|
||||||
`"p":{"format":"{first} {/thing.q}","first":["A","B"]},"q":{"format":"{a}","a":["1","2"]}}`,
|
"reference in the head's own format": {
|
||||||
"the mirror shape": `{"format":"{p.first} {q.a}",` +
|
"thing": `{"format":"{inner}","inner":{"format":"{p.first} {q.a}",` +
|
||||||
`"p":{"format":"{first}","first":["A","B"]},"q":{"format":"{a} {/thing.p}","a":["1","2"]}}`,
|
`"p":{"format":"{first} {/hop}","first":["A","B"]},"q":{"format":"{a}","a":["1","2"]}}}`,
|
||||||
|
"hop": `"{/thing.inner.q}"`,
|
||||||
|
},
|
||||||
|
"the mirror shape": {
|
||||||
|
"thing": `{"format":"{inner}","inner":{"format":"{p.first} {q.a}",` +
|
||||||
|
`"p":{"format":"{first}","first":["A","B"]},"q":{"format":"{a} {/hop}","a":["1","2"]}}}`,
|
||||||
|
"hop": `"{/thing.inner.p}"`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, file := range accepted {
|
for name, files := range accepted {
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"thing": file}))); err != nil {
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
||||||
t.Errorf("%s: New = %v, want it accepted", name, err)
|
t.Errorf("%s: New = %v, want it accepted", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -673,7 +713,8 @@ func TestCycleThroughAPathTokenIsRejected(t *testing.T) {
|
|||||||
// must be caught at New. Reaching render would be fatal: the recursion never
|
// must be caught at New. Reaching render would be fatal: the recursion never
|
||||||
// terminates, and a stack overflow cannot be recovered.
|
// terminates, and a stack overflow cannot be recovered.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"a": `{"format":"{p.x}","p":{"format":"{x}","x":"{/a}"}}`,
|
"a": `{"format":"{p.x}","p":{"format":"{x}","x":"{/b}"}}`,
|
||||||
|
"b": `"{/a}"`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||||
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
||||||
|
|||||||
+5
-9
@@ -217,18 +217,14 @@ func TestRecordRejectsOverlappingReferenceColumns(t *testing.T) {
|
|||||||
{"as a builtin operand", `{"format":"","whole":"{uppercase(/cat.a)}","inner":"{/cat.a.b}"}`},
|
{"as a builtin operand", `{"format":"","whole":"{uppercase(/cat.a)}","inner":"{/cat.a.b}"}`},
|
||||||
{"a bare reference beside a path", `{"format":"","whole":"{/cat}","inner":"{/cat.a.b}"}`},
|
{"a bare reference beside a path", `{"format":"","whole":"{/cat}","inner":"{/cat.a.b}"}`},
|
||||||
} {
|
} {
|
||||||
f := newGenerator(t, writeData(t, map[string]string{"cat": cat, "row": c.row}), WithSeed(1))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": c.row})))
|
||||||
_, err := f.FakeRecord("row")
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
||||||
t.Errorf("%s: FakeRecord = %v, want the overlap rejected the way one format is", c.name, err)
|
t.Errorf("%s: New = %v, want the overlap refused at load, the way one format is", c.name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), `"whole"`) || !strings.Contains(err.Error(), `"inner"`) {
|
if !strings.Contains(err.Error(), `"whole"`) || !strings.Contains(err.Error(), `"inner"`) {
|
||||||
t.Errorf("%s: error %q names neither column; it must name both", c.name, err)
|
t.Errorf("%s: error %q names neither column; it must name both", c.name, err)
|
||||||
}
|
}
|
||||||
if _, err := f.Fake("row"); err != nil {
|
|
||||||
t.Errorf("%s: Fake(row) = %v, want the string view untouched", c.name, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,9 +246,9 @@ func TestRecordRejectsAColumnReadingItsOwnRecord(t *testing.T) {
|
|||||||
{"the record as an operand", `"up":"{uppercase(/person)}"`},
|
{"the record as an operand", `"up":"{uppercase(/person)}"`},
|
||||||
} {
|
} {
|
||||||
person := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"],` + c.column + `}`
|
person := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"],` + c.column + `}`
|
||||||
f := newGenerator(t, writeData(t, map[string]string{"person": person}), WithSeed(1))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"person": person})))
|
||||||
if _, err := f.FakeRecord("person"); err == nil || !strings.Contains(err.Error(), "points back at this record") {
|
if err == nil || !strings.Contains(err.Error(), "names the category it sits in") {
|
||||||
t.Errorf("%s: FakeRecord = %v, want it refused; the column would contradict the columns beside it", c.name, err)
|
t.Errorf("%s: New = %v, want it refused at load; the column would contradict the columns beside it", c.name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-36
@@ -83,25 +83,23 @@ func TestReferenceErrors(t *testing.T) {
|
|||||||
"card": `"{/who.f}"`,
|
"card": `"{/who.f}"`,
|
||||||
},
|
},
|
||||||
"empty reference path": {"card": `"{/}"`},
|
"empty reference path": {"card": `"{/}"`},
|
||||||
// A reference that leads back to its own value never terminates at render,
|
// A reference that leads back to its own value never terminates at render, so
|
||||||
// so New must reject the cycle up front (direct, mutual, or chained).
|
// New must reject the cycle up front (mutual or chained). One into its own
|
||||||
"direct cycle": {"a": `"x{/a}"`},
|
// category is refused before the cycle walk reaches it, as a unit rule.
|
||||||
|
"a category referencing itself": {"a": `"x{/a}"`},
|
||||||
"mutual cycle": {"a": `"{/b}"`, "b": `"{/a}"`},
|
"mutual cycle": {"a": `"{/b}"`, "b": `"{/a}"`},
|
||||||
"chain cycle": {"a": `"{/b}"`, "b": `"{/c}"`, "c": `"{/a}"`},
|
"chain cycle": {"a": `"{/b}"`, "b": `"{/c}"`, "c": `"{/a}"`},
|
||||||
// calc renders its operands, so a cycle through one must be caught too.
|
// calc renders its operands, so a reference through one is caught too.
|
||||||
"calc operand cycle": {"x": `{"format":"{calc(y)}","y":"{/x}"}`},
|
"a calc operand into its own category": {"x": `{"format":"{calc(y)}","y":"{/x}"}`},
|
||||||
// A field its parent's format never renders is still reachable by dot path,
|
// 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.
|
// so what hides in one must fail at New rather than at render.
|
||||||
"cycle in an unrendered field": {"cat": `{"format":"hi","x":"{/cat.x}"}`},
|
"an unrendered field into its own category": {"cat": `{"format":"hi","x":"{/cat.x}"}`},
|
||||||
"mutual cycle between unrendered fields": {
|
"two unrendered fields into their own category": {
|
||||||
"cat": `{"format":"hi","x":"{/cat.y}","y":"{/cat.x}"}`,
|
"cat": `{"format":"hi","x":"{/cat.y}","y":"{/cat.x}"}`,
|
||||||
},
|
},
|
||||||
"cycle in an unrendered field of a choice arm": {
|
// The shipped layout puts categories in folders, so one level down is the
|
||||||
"cat": `{"format":"hi","x":"{/cat.x}"}`,
|
// common case, not an edge case.
|
||||||
},
|
"a category in a subfolder referencing itself": {"sv_SE/a": `"x{/sv_SE.a}"`},
|
||||||
// The shipped layout puts categories in folders, so a cycle one level down
|
|
||||||
// is the common case, not an edge case.
|
|
||||||
"cycle in a subfolder": {"sv_SE/a": `"x{/sv_SE.a}"`},
|
|
||||||
"mutual cycle within a subfolder": {"sv_SE/a": `"{/sv_SE.b}"`, "sv_SE/b": `"{/sv_SE.a}"`},
|
"mutual cycle within a subfolder": {"sv_SE/a": `"{/sv_SE.b}"`, "sv_SE/b": `"{/sv_SE.a}"`},
|
||||||
"mutual cycle across two folders": {"en_US/a": `"{/sv_SE.b}"`, "sv_SE/b": `"{/en_US.a}"`},
|
"mutual cycle across two folders": {"en_US/a": `"{/sv_SE.b}"`, "sv_SE/b": `"{/en_US.a}"`},
|
||||||
// ".." is reserved for bound references, so an authored key using it would
|
// ".." is reserved for bound references, so an authored key using it would
|
||||||
@@ -131,17 +129,26 @@ func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReferenceFromUnrenderedFieldTerminates guards the cycle check against
|
// TestReferenceIntoItsOwnCategoryIsRejected pins the unit a category is: a reference
|
||||||
// over-rejecting: a field the format never renders may point back at its own
|
// back into it describes a draw other than the fields beside it, whether the format
|
||||||
// category, which terminates, and stays renderable by path.
|
// renders that field or not, so it is refused at load rather than left to disagree in
|
||||||
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
// the record view.
|
||||||
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":"see {/cat}"}`})
|
func TestReferenceIntoItsOwnCategoryIsRejected(t *testing.T) {
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
for name, file := range map[string]string{
|
||||||
if got := fake(t, f, "cat"); got != "hi" {
|
"the category whole": `{"format":"hi","x":"see {/cat}"}`,
|
||||||
t.Fatalf("cat = %q, want hi", got)
|
"a field of its own": `{"format":"hi","x":"{/cat.y}","y":"1"}`,
|
||||||
|
"a path the format reads": `{"format":"{/cat.y}","y":"1"}`,
|
||||||
|
} {
|
||||||
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "names the category it sits in") {
|
||||||
|
t.Errorf("%s: New = %v, want the reference into its own category refused", name, err)
|
||||||
}
|
}
|
||||||
if got := fake(t, f, "cat.x"); got != "see hi" {
|
}
|
||||||
t.Fatalf("cat.x = %q, want \"see hi\"", got)
|
// Reading a sibling as a path is the spelling that stays.
|
||||||
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
|
"cat": `{"format":"{y.v}","y":{"format":"{v}","v":["1","2"]}}`,
|
||||||
|
}))); err != nil {
|
||||||
|
t.Errorf("New = %v, want the sibling path accepted", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +196,9 @@ func TestNewErrorPathIsCanonical(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"cycle inside a choice arm",
|
"cycle through another category",
|
||||||
map[string]string{"cat": `{"format":"hi","x":"{/cat.x}"}`},
|
map[string]string{"cat": `{"format":"hi","x":"{/hop}"}`, "hop": `"{/cat.x}"`},
|
||||||
"fejkdata: reference cycle: cat.x -> /cat.x",
|
"fejkdata: reference cycle: cat.x -> /hop -> /cat.x",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bad reference reached through another reference",
|
"bad reference reached through another reference",
|
||||||
@@ -255,21 +262,30 @@ func TestBareReferenceDrawsEachTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReferenceOverlapIsRejected(t *testing.T) {
|
func TestReferenceOverlapIsRejected(t *testing.T) {
|
||||||
p := `[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]`
|
// A category never references itself, so the reads sit in a second category.
|
||||||
for name, file := range map[string]string{
|
cat := `{"format":"x","p":[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]}`
|
||||||
"head beside a path": `{"format":"{/cat.p} {/cat.p.first}","p":` + p + `}`,
|
for name, row := range map[string]string{
|
||||||
"sibling path beside a reference path": `{"format":"{p.first} {/cat.p.last}","p":` + p + `}`,
|
"head beside a path": `"{/cat.p} {/cat.p.first}"`,
|
||||||
"sibling fields reading a level and a path into it": `{"format":"{a} {b}","a":"{/cat.p}","b":"{/cat.p.first}","p":` + p + `}`,
|
"sibling fields reading a level and a path into it": `{"format":"{a} {b}","a":"{/cat.p}","b":"{/cat.p.first}"}`,
|
||||||
"a field rendering the level a nested reference reads into": `{"format":"{x} {p}","x":"{/cat.p.first}","p":` + p + `}`,
|
"a field rendering the level a nested reference reads into": `{"format":"{x} {p}","x":"{/cat.p.first}","p":"{/cat.p}"}`,
|
||||||
"a bare reference beside a path into what it never renders": `{"format":"{a} {b}","a":"{/other}","b":"{/other.p.first}"}`,
|
"a bare reference beside a path into what it never renders": `{"format":"{a} {b}","a":"{/cat}","b":"{/cat.p.first}"}`,
|
||||||
} {
|
} {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file, "other": `{"format":"x","p":` + p + `}`})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
||||||
t.Errorf("%s: New = %v, want the overlap rejected", name, err)
|
t.Errorf("%s: New = %v, want the overlap rejected", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A sibling path and a reference into the level it holds are the same overlap,
|
||||||
|
// and the reference reaches it from a category row renders.
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{a} {b}","a":{"format":"{/cat.p}","drawGroup":"g"},"b":"{/cat.p.first}","p":` + p + `}`,
|
"row": `{"format":"{p.first} {/hop}","p":[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]}`,
|
||||||
|
"hop": `"{/row.p.last}"`,
|
||||||
|
}))); err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
||||||
|
t.Errorf("New = %v, want a sibling path beside a reference into it rejected", err)
|
||||||
|
}
|
||||||
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
|
"cat": cat,
|
||||||
|
"row": `{"format":"{a} {b}","a":{"format":"{/cat.p}","drawGroup":"g"},"b":"{/cat.p.first}"}`,
|
||||||
}))); err != nil {
|
}))); err != nil {
|
||||||
t.Errorf("New = %v, want a level and a path into it accepted in groups of their own", err)
|
t.Errorf("New = %v, want a level and a path into it accepted in groups of their own", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user