GNU-style CLI flags, embedded data, and a literal format grammar #3
+4
-9
@@ -43,7 +43,7 @@ func tmpData(b *testing.B, name, body string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkCalc(b *testing.B) {
|
func BenchmarkCalc(b *testing.B) {
|
||||||
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99"],"qty":["3"]}`)
|
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`)
|
||||||
benchPath(b, dir, "inv")
|
benchPath(b, dir, "inv")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,24 +61,19 @@ func BenchmarkRepeat(b *testing.B) {
|
|||||||
// what a correlated pair costs against BenchmarkUnbound, the same output drawn from
|
// what a correlated pair costs against BenchmarkUnbound, the same output drawn from
|
||||||
// two independent fields.
|
// two independent fields.
|
||||||
func BenchmarkBound(b *testing.B) {
|
func BenchmarkBound(b *testing.B) {
|
||||||
dir := tmpData(b, "addr", `{"format":"{place.postal-code} {place.locality}","place":[
|
dir := tmpData(b, "addr", `{"format":"{place.postal-code} {place.locality}","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`)
|
||||||
{"format":"{locality}","locality":"Stockholm","postal-code":{"format":"1{digits(2)} {digits(2)}"}},
|
|
||||||
{"format":"{locality}","locality":"Tranås","postal-code":{"format":"573 {digits(2)}"}}]}`)
|
|
||||||
benchPath(b, dir, "addr")
|
benchPath(b, dir, "addr")
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkUnbound(b *testing.B) {
|
func BenchmarkUnbound(b *testing.B) {
|
||||||
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}",
|
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}","postal-code":["1{digits(2)} {digits(2)}","573 {digits(2)}"],"locality":["Stockholm","Tranås"]}`)
|
||||||
"postal-code":[{"format":"1{digits(2)} {digits(2)}"},{"format":"573 {digits(2)}"}],
|
|
||||||
"locality":["Stockholm","Tranås"]}`)
|
|
||||||
benchPath(b, dir, "addr")
|
benchPath(b, dir, "addr")
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkBoundDeep reads two paths through an intermediate level, the shape the
|
// BenchmarkBoundDeep reads two paths through an intermediate level, the shape the
|
||||||
// depth question is about.
|
// depth question is about.
|
||||||
func BenchmarkBoundDeep(b *testing.B) {
|
func BenchmarkBoundDeep(b *testing.B) {
|
||||||
dir := tmpData(b, "addr", `{"format":"{p.addr.city} {p.addr.zip}","p":[
|
dir := tmpData(b, "addr", `{"format":"{p.addr.city} {p.addr.zip}","p":{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":"1{digits(2)} {digits(2)}"}}}`)
|
||||||
{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":{"format":"1{digits(2)} {digits(2)}"}}}]}`)
|
|
||||||
benchPath(b, dir, "addr")
|
benchPath(b, dir, "addr")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-59
@@ -15,10 +15,7 @@ import (
|
|||||||
|
|
||||||
// swedishPlaces is a two-variant sibling whose variants pair a locality with the
|
// swedishPlaces is a two-variant sibling whose variants pair a locality with the
|
||||||
// postal-code prefix that really belongs to it.
|
// postal-code prefix that really belongs to it.
|
||||||
const swedishPlaces = `{"format":"%s","place":[
|
const swedishPlaces = `{"format":"%s","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`
|
||||||
{"format":"{locality}","locality":"Stockholm","postal-code":{"format":"1{digits(2)} {digits(2)}"}},
|
|
||||||
{"format":"{locality}","locality":"Tranås","postal-code":{"format":"573 {digits(2)}"}}
|
|
||||||
]}`
|
|
||||||
|
|
||||||
// agree reports whether a rendered "postcode locality" pair is a real pairing.
|
// agree reports whether a rendered "postcode locality" pair is a real pairing.
|
||||||
var agree = regexp.MustCompile(`^(1[0-9]{2} [0-9]{2} Stockholm|573 [0-9]{2} Tranås)$`)
|
var agree = regexp.MustCompile(`^(1[0-9]{2} [0-9]{2} Stockholm|573 [0-9]{2} Tranås)$`)
|
||||||
@@ -48,10 +45,10 @@ func TestRenderingALevelAndReadingIntoItIsRejected(t *testing.T) {
|
|||||||
// other rendered — so the pair is a load error rather than a shape whose two
|
// other rendered — so the pair is a load error rather than a shape whose two
|
||||||
// halves can disagree.
|
// halves can disagree.
|
||||||
rejected := map[string]string{
|
rejected := map[string]string{
|
||||||
"bare head beside a path": `{"format":"{p} + {p.first}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"bare head beside a path": `{"format":"{p} + {p.first}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
"prefix path beside a path": `{"format":"{p.addr}|{p.addr.city}","p":[{"format":"{addr}","addr":[` +
|
"prefix path beside a path": `{"format":"{p.addr}|{p.addr.city}","p":{"format":"{addr}","addr":[` +
|
||||||
`{"format":"{city}","city":["Kiruna","Boden"]},{"format":"{city}","city":["Malmö","Lund"]}]}]}`,
|
`{"format":"{city}","city":["Kiruna","Boden"]},{"format":"{city}","city":["Malmö","Lund"]}]}}`,
|
||||||
"either order": `{"format":"{p.first} + {p}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"either order": `{"format":"{p.first} + {p}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, file := range rejected {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||||
@@ -65,7 +62,7 @@ func TestRenderingALevelAndReadingIntoItIsRejected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// The same path twice is one spelling, so it stays legal.
|
// The same path twice is one spelling, so it stays legal.
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{p.first} + {p.first}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"cat": `{"format":"{p.first} + {p.first}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
}))); err != nil {
|
}))); err != nil {
|
||||||
t.Errorf("New = %v, want one path read twice accepted", err)
|
t.Errorf("New = %v, want one path read twice accepted", err)
|
||||||
}
|
}
|
||||||
@@ -84,7 +81,7 @@ func TestCalcOperandNamingABoundLevelIsRejected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Arithmetic over fields no path names is untouched.
|
// Arithmetic over fields no path names is untouched.
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99"],"qty":["3"]}`,
|
"cat": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`,
|
||||||
}))); err != nil {
|
}))); err != nil {
|
||||||
t.Errorf("New = %v, want plain arithmetic accepted", err)
|
t.Errorf("New = %v, want plain arithmetic accepted", err)
|
||||||
}
|
}
|
||||||
@@ -95,13 +92,13 @@ func TestReferenceNamingABoundLevelIsRejected(t *testing.T) {
|
|||||||
// 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{
|
rejected := map[string]string{
|
||||||
"reference names the head": `{"format":"{p.first}|{..cat.p}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"reference names the head": `{"format":"{p.first}|{..cat.p}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
"reference names the leaf": `{"format":"{p.addr}|{..cat.p.addr}","p":{"format":"x","addr":["A","B","C","D"]}}`,
|
"reference names the leaf": `{"format":"{p.addr}|{..cat.p.addr}","p":{"format":"x","addr":["A","B","C","D"]}}`,
|
||||||
// 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": `{"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":{"format":"{..cat.p}"}}`,
|
`"inner":"{..cat.p}"}`,
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, file := range rejected {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||||
@@ -111,7 +108,7 @@ func TestReferenceNamingABoundLevelIsRejected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// A reference to anything this format does not bind is untouched.
|
// A reference to anything this format does not bind is untouched.
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{p.first} {..surname}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`,
|
"cat": `{"format":"{p.first} {..surname}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||||
"surname": `["Eriksson","Lindqvist"]`,
|
"surname": `["Eriksson","Lindqvist"]`,
|
||||||
}))); err != nil {
|
}))); err != nil {
|
||||||
t.Errorf("New = %v, want a reference outside the bound level accepted", err)
|
t.Errorf("New = %v, want a reference outside the bound level accepted", err)
|
||||||
@@ -123,7 +120,7 @@ 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":{"format":"{..a}"}}}`,
|
"a": `{"format":"{p.x}","p":{"format":"static","x":"{..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)
|
||||||
@@ -134,7 +131,7 @@ 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":{"format":"{..thing.q}"}},` +
|
"thing": `{"format":"{p.a} {q.x}","p":{"format":"static","a":"{..thing.q}"},` +
|
||||||
`"q":{"format":"{x}","x":["1","2"]}}`,
|
`"q":{"format":"{x}","x":["1","2"]}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
if err == nil || !strings.Contains(err.Error(), "reads a path into") {
|
||||||
@@ -161,7 +158,7 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"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":{"format":"{..cat.net}"}}`,
|
`"q":"{..cat.net}"}`,
|
||||||
},
|
},
|
||||||
`{q} renders "net"`,
|
`{q} renders "net"`,
|
||||||
},
|
},
|
||||||
@@ -169,8 +166,8 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
// so wrapping the operand in one changes nothing.
|
// so wrapping the operand in one changes nothing.
|
||||||
"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":{"format":"{..cat.n}"}}`,
|
`"q":"{..cat.n}"}`,
|
||||||
},
|
},
|
||||||
`{q} renders "n"`,
|
`{q} renders "n"`,
|
||||||
},
|
},
|
||||||
@@ -179,7 +176,7 @@ 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":{"format":"{..cat.a}"}}`,
|
`"b":"{..cat.a}"}`,
|
||||||
},
|
},
|
||||||
`calc operand "b" renders "a"`,
|
`calc operand "b" renders "a"`,
|
||||||
},
|
},
|
||||||
@@ -196,7 +193,7 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
|||||||
"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":{"format":"{..cat.net.v}"}}`,
|
`"q":"{..cat.net.v}"}`,
|
||||||
},
|
},
|
||||||
`{q} renders "net"`,
|
`{q} renders "net"`,
|
||||||
},
|
},
|
||||||
@@ -206,7 +203,7 @@ 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":{"format":"{..cat.b}"}}`,
|
`"b":{"format":"{y}","y":["3","4"]},"w":"{..cat.b}"}`,
|
||||||
},
|
},
|
||||||
`{w} renders "b"`,
|
`{w} renders "b"`,
|
||||||
},
|
},
|
||||||
@@ -233,20 +230,20 @@ 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":{"format":"{..cat.net.spare}"}}`,
|
`"unit":"{..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.
|
||||||
"two operands sharing one source": {
|
"two operands sharing one source": {
|
||||||
"die": `["1","2","3","4","5","6"]`,
|
"die": `["1","2","3","4","5","6"]`,
|
||||||
"cat": `{"format":"{d1} + {d2} = {calc(d1 + d2, 0)}","d1":{"format":"{..die}"},` +
|
"cat": `{"format":"{d1} + {d2} = {calc(d1 + d2, 0)}","d1":"{..die}",` +
|
||||||
`"d2":{"format":"{..die}"}}`,
|
`"d2":"{..die}"}`,
|
||||||
},
|
},
|
||||||
// Likewise a reference drawing from what the operand draws from: {..common}
|
// Likewise a reference drawing from what the operand draws from: {..common}
|
||||||
// and net are two names, not two spellings of one field.
|
// and net are two names, not two spellings of one field.
|
||||||
"a reference to what an operand renders through": {
|
"a reference to what an operand renders through": {
|
||||||
"common": `["1","2"]`,
|
"common": `["1","2"]`,
|
||||||
"cat": `{"format":"{calc(net * 2, 2)} {..common}","net":{"format":"{..common}"}}`,
|
"cat": `{"format":"{calc(net * 2, 2)} {..common}","net":"{..common}"}`,
|
||||||
},
|
},
|
||||||
// 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": {
|
||||||
@@ -272,11 +269,11 @@ func TestAPathReachesEveryVariantItMightDraw(t *testing.T) {
|
|||||||
// to a held level disagrees silently.
|
// to a held level disagrees silently.
|
||||||
rejected := map[string]struct{ file, want string }{
|
rejected := map[string]struct{ file, 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":{"format":"{..cat}"}}]}`,
|
`{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{..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":{"format":"{..cat.q}"}}],` +
|
`{"format":"{p.x} {q.y}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{..cat.q}"}],` +
|
||||||
`"q":{"format":"{y}","y":["1","2"]}}`,
|
`"q":{"format":"{y}","y":["1","2"]}}`,
|
||||||
"reads a path into",
|
"reads a path into",
|
||||||
},
|
},
|
||||||
@@ -309,10 +306,10 @@ func TestALevelAPathNeverRendersIsAccepted(t *testing.T) {
|
|||||||
func TestADeepDiamondChainLoads(t *testing.T) {
|
func TestADeepDiamondChainLoads(t *testing.T) {
|
||||||
// A diamond chain is 2^n routes through n nodes. The search past a bound level
|
// A diamond chain is 2^n routes through n nodes. The search past a bound level
|
||||||
// must walk each node once, not once per route, or a deep chain never loads.
|
// must walk each node once, not once per route, or a deep chain never loads.
|
||||||
files := map[string]string{"l0": `["x"]`}
|
files := map[string]string{"l0": `"x"`}
|
||||||
for i := 1; i <= 30; i++ {
|
for i := 1; i <= 30; i++ {
|
||||||
files[fmt.Sprintf("l%d", i)] = fmt.Sprintf(
|
files[fmt.Sprintf("l%d", i)] = fmt.Sprintf(
|
||||||
`{"format":"{a}{b}","a":{"format":"{..l%d}"},"b":{"format":"{..l%d}"}}`, i-1, i-1)
|
`{"format":"{a}{b}","a":"{..l%d}","b":"{..l%d}"}`, i-1, i-1)
|
||||||
}
|
}
|
||||||
files["thing"] = `{"format":"{p.first} {..l30}","p":{"format":"x","first":["A","B"]}}`
|
files["thing"] = `{"format":"{p.first} {..l30}","p":{"format":"x","first":["A","B"]}}`
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
||||||
@@ -325,9 +322,9 @@ func TestASharedNodeIsWalkedOnce(t *testing.T) {
|
|||||||
// The search past a bound level must take that in its stride rather than walk
|
// The search past a bound level must take that in its stride rather than walk
|
||||||
// the shared node once per route.
|
// the shared node once per route.
|
||||||
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{p.first}|{q}","p":[{"format":"{first}","first":["Anna","Bo"]}],` +
|
"cat": `{"format":"{p.first}|{q}","p":{"format":"{first}","first":["Anna","Bo"]},` +
|
||||||
`"q":{"format":"{a}{b}","a":{"format":"{..shared}"},"b":{"format":"{..shared}"}}}`,
|
`"q":{"format":"{a}{b}","a":"{..shared}","b":"{..shared}"}}`,
|
||||||
"shared": `["x"]`,
|
"shared": `"x"`,
|
||||||
})), WithSeed(1))
|
})), WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New = %v, want a shared node accepted", err)
|
t.Fatalf("New = %v, want a shared node accepted", err)
|
||||||
@@ -342,7 +339,7 @@ func TestTheEarlierReaderIsNamed(t *testing.T) {
|
|||||||
// first is the one reported, so the error points at the same place a reader
|
// first is the one reported, so the error points at the same place a reader
|
||||||
// looking at the format would start.
|
// looking at the format would start.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{calc(p * 1)} {p} {p.first}","p":[{"format":"{first}","first":["1","2"]}]}`,
|
"cat": `{"format":"{calc(p * 1)} {p} {p.first}","p":{"format":"{first}","first":["1","2"]}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), `calc operand "p"`) {
|
if err == nil || !strings.Contains(err.Error(), `calc operand "p"`) {
|
||||||
t.Fatalf("New = %v, want the calc operand named, being written first", err)
|
t.Fatalf("New = %v, want the calc operand named, being written first", err)
|
||||||
@@ -368,7 +365,7 @@ func TestNestedChoiceDrawsOneVariant(t *testing.T) {
|
|||||||
f := engine(21)
|
f := engine(21)
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
seen[mustRender(t, f, `{"format":"{p.x}","p":[[{"format":"{x}","x":"1"}],[{"format":"{x}","x":"2"}]]}`)] = true
|
seen[mustRender(t, f, `{"format":"{p.x}","p":[{"format":"{x}","x":"1"},{"format":"{x}","x":"2"}]}`)] = true
|
||||||
}
|
}
|
||||||
if !seen["1"] || !seen["2"] || len(seen) != 2 {
|
if !seen["1"] || !seen["2"] || len(seen) != 2 {
|
||||||
t.Fatalf("200 draws produced %v, want 1 and 2", seen)
|
t.Fatalf("200 draws produced %v, want 1 and 2", seen)
|
||||||
@@ -379,7 +376,7 @@ func TestRepeatingLevelIsNamedInTheError(t *testing.T) {
|
|||||||
// The level carrying the repeat is the one to fix, so the error names it
|
// The level carrying the repeat is the one to fix, so the error names it
|
||||||
// rather than the head the path started from.
|
// rather than the head the path started from.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"[{p.a.b}]","p":{"format":"{a}","a":{"format":"{b}","repeat":3,"separator":",","b":["z"]}}}`,
|
"cat": `{"format":"[{p.a.b}]","p":{"format":"{a}","a":{"format":"{b}","repeat":3,"separator":",","b":"z"}}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), `"p.a"`) {
|
if err == nil || !strings.Contains(err.Error(), `"p.a"`) {
|
||||||
t.Fatalf("New = %v, want it to name the level p.a that carries the repeat", err)
|
t.Fatalf("New = %v, want it to name the level p.a that carries the repeat", err)
|
||||||
@@ -391,7 +388,7 @@ func TestPathIntoARepeatingLevelBehindAChoiceIsRejected(t *testing.T) {
|
|||||||
// reach inside one — otherwise the direct spelling is a load error and the same
|
// reach inside one — otherwise the direct spelling is a load error and the same
|
||||||
// mistake behind a choice silently drops the repeat.
|
// mistake behind a choice silently drops the repeat.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"[{p.a}]","p":[{"format":"{a}","repeat":3,"separator":",","a":["x"]},{"format":"{a}","a":["y"]}]}`,
|
"cat": `{"format":"[{p.a}]","p":[{"format":"{a}","repeat":3,"separator":",","a":"x"},{"format":"{a}","a":"y"}]}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
||||||
t.Fatalf("New = %v, want the repeat behind a choice rejected", err)
|
t.Fatalf("New = %v, want the repeat behind a choice rejected", err)
|
||||||
@@ -402,7 +399,7 @@ func TestPathIntoARepeatingLevelIsRejected(t *testing.T) {
|
|||||||
// A path reads one level's draw, so it can never apply that level's repeat.
|
// A path reads one level's draw, so it can never apply that level's repeat.
|
||||||
// The engine rejects options that cannot take effect, and this is one.
|
// The engine rejects options that cannot take effect, and this is one.
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||||
"cat": `{"format":"[{p.a}]","p":{"format":"{a}","repeat":3,"separator":",","a":["z"]}}`,
|
"cat": `{"format":"[{p.a}]","p":{"format":"{a}","repeat":3,"separator":",","a":"z"}}`,
|
||||||
})))
|
})))
|
||||||
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
||||||
t.Fatalf("New = %v, want a path into a repeating level rejected", err)
|
t.Fatalf("New = %v, want a path into a repeating level rejected", err)
|
||||||
@@ -511,7 +508,7 @@ func TestDottedTokenErrors(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
"unknown head": {
|
"unknown head": {
|
||||||
`{"format":"{nope.x}","place":[{"format":"{v}","v":"A"}]}`,
|
`{"format":"{nope.x}","place":{"format":"{v}","v":"A"}}`,
|
||||||
`no field "nope"`,
|
`no field "nope"`,
|
||||||
},
|
},
|
||||||
"unknown tail": {
|
"unknown tail": {
|
||||||
@@ -519,7 +516,7 @@ func TestDottedTokenErrors(t *testing.T) {
|
|||||||
`"nope"`,
|
`"nope"`,
|
||||||
},
|
},
|
||||||
"tail only some variants carry": {
|
"tail only some variants carry": {
|
||||||
`{"format":"{place.locality}","place":[{"format":"{locality}","locality":"A"},{"format":"x"}]}`,
|
`{"format":"{place.locality}","place":[{"format":"{locality}","locality":"A"},"x"]}`,
|
||||||
`locality`,
|
`locality`,
|
||||||
},
|
},
|
||||||
"path into a literal": {
|
"path into a literal": {
|
||||||
@@ -527,7 +524,7 @@ func TestDottedTokenErrors(t *testing.T) {
|
|||||||
`"y"`,
|
`"y"`,
|
||||||
},
|
},
|
||||||
"dotted field key": {
|
"dotted field key": {
|
||||||
`{"format":"[{a.b}]","a.b":["V"]}`,
|
`{"format":"[{a.b}]","a.b":"V"}`,
|
||||||
`contains "."`,
|
`contains "."`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -548,8 +545,7 @@ func TestSamePathReadTwiceReadsOneValue(t *testing.T) {
|
|||||||
// what a name shown in a display form and again in an address needs.
|
// what a name shown in a display form and again in an address needs.
|
||||||
f := engine(7)
|
f := engine(7)
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
got := mustRender(t, f, `{"format":"{p.first}|{p.first}",
|
got := mustRender(t, f, `{"format":"{p.first}|{p.first}","p":{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}}`)
|
||||||
"p":[{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}]}`)
|
|
||||||
parts := strings.Split(got, "|")
|
parts := strings.Split(got, "|")
|
||||||
if parts[0] != parts[1] {
|
if parts[0] != parts[1] {
|
||||||
t.Fatalf("draw %d = %q, want one value read twice", i, got)
|
t.Fatalf("draw %d = %q, want one value read twice", i, got)
|
||||||
@@ -571,9 +567,7 @@ func TestDifferentTailsUnderOneHeadShareTheRow(t *testing.T) {
|
|||||||
|
|
||||||
// deepPlaces nests the correlated facts a level down: the row holds an addr, and
|
// deepPlaces nests the correlated facts a level down: the row holds an addr, and
|
||||||
// the addr holds the city and the zip that belongs to it.
|
// the addr holds the city and the zip that belongs to it.
|
||||||
const deepPlaces = `{"format":"%s","p":[{"format":"{addr}","addr":[
|
const deepPlaces = `{"format":"%s","p":{"format":"{addr}","addr":[{"format":"{city}","city":"Stockholm","zip":"11111"},{"format":"{city}","city":"Kiruna","zip":"98100"}]}}`
|
||||||
{"format":"{city}","city":"Stockholm","zip":"11111"},
|
|
||||||
{"format":"{city}","city":"Kiruna","zip":"98100"}]}]}`
|
|
||||||
|
|
||||||
func TestPathHoldsItsDrawAtEveryLevel(t *testing.T) {
|
func TestPathHoldsItsDrawAtEveryLevel(t *testing.T) {
|
||||||
// A choice below the head is drawn once too, so two paths sharing a prefix
|
// A choice below the head is drawn once too, so two paths sharing a prefix
|
||||||
@@ -595,13 +589,7 @@ func TestPathHoldsItsDrawAtEveryLevel(t *testing.T) {
|
|||||||
func TestPathHoldsItsDrawThreeLevelsDown(t *testing.T) {
|
func TestPathHoldsItsDrawThreeLevelsDown(t *testing.T) {
|
||||||
// The rule does not run out at two: every level a path passes through is held.
|
// The rule does not run out at two: every level a path passes through is held.
|
||||||
f := engine(12)
|
f := engine(12)
|
||||||
tmpl := `{"format":"{p.geo.town.name}/{p.geo.town.zip} {p.geo.region}","p":[{"format":"{geo}","geo":[
|
tmpl := `{"format":"{p.geo.town.name}/{p.geo.town.zip} {p.geo.region}","p":{"format":"{geo}","geo":[{"format":"{town}","region":"Norrbotten","town":[{"format":"{name}","name":"Kiruna","zip":"98100"},{"format":"{name}","name":"Luleå","zip":"97200"}]},{"format":"{town}","region":"Skåne","town":[{"format":"{name}","name":"Malmö","zip":"21100"},{"format":"{name}","name":"Lund","zip":"22100"}]}]}}`
|
||||||
{"format":"{town}","region":"Norrbotten","town":[
|
|
||||||
{"format":"{name}","name":"Kiruna","zip":"98100"},
|
|
||||||
{"format":"{name}","name":"Luleå","zip":"97200"}]},
|
|
||||||
{"format":"{town}","region":"Skåne","town":[
|
|
||||||
{"format":"{name}","name":"Malmö","zip":"21100"},
|
|
||||||
{"format":"{name}","name":"Lund","zip":"22100"}]}]}]}`
|
|
||||||
want := map[string]bool{
|
want := map[string]bool{
|
||||||
"Kiruna/98100 Norrbotten": true, "Luleå/97200 Norrbotten": true,
|
"Kiruna/98100 Norrbotten": true, "Luleå/97200 Norrbotten": true,
|
||||||
"Malmö/21100 Skåne": true, "Lund/22100 Skåne": true,
|
"Malmö/21100 Skåne": true, "Lund/22100 Skåne": true,
|
||||||
@@ -641,9 +629,7 @@ func TestDeepPathsUnderOneHeadStayIndependentWhereTheyDiverge(t *testing.T) {
|
|||||||
// Two paths share only what they actually share: a common prefix is one draw,
|
// Two paths share only what they actually share: a common prefix is one draw,
|
||||||
// and each keeps its own draw past the point they part.
|
// and each keeps its own draw past the point they part.
|
||||||
f := engine(14)
|
f := engine(14)
|
||||||
tmpl := `{"format":"{p.a.v}{p.b.v}","p":[{"format":"x",
|
tmpl := `{"format":"{p.a.v}{p.b.v}","p":{"format":"x","a":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}],"b":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}]}}`
|
||||||
"a":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}],
|
|
||||||
"b":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}]}]}`
|
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 400; i++ {
|
for i := 0; i < 400; i++ {
|
||||||
seen[mustRender(t, f, tmpl)] = true
|
seen[mustRender(t, f, tmpl)] = true
|
||||||
@@ -660,9 +646,9 @@ func TestEmptyPathSegmentIsRejected(t *testing.T) {
|
|||||||
// nothing is reported as that rather than as a missing field. An empty name is
|
// nothing is reported as that rather than as a missing field. An empty name is
|
||||||
// rejected where it is authored, so no data can make these resolve.
|
// rejected where it is authored, so no data can make these resolve.
|
||||||
rejected := map[string]string{
|
rejected := map[string]string{
|
||||||
"trailing dot": `{"format":"[{a.}]","a":{"format":"x"}}`,
|
"trailing dot": `{"format":"[{a.}]","a":"x"}`,
|
||||||
"leading dot": `{"format":"[{.b}]","a":{"format":"{b}","b":["V"]}}`,
|
"leading dot": `{"format":"[{.b}]","a":{"format":"{b}","b":"V"}}`,
|
||||||
"double dot": `{"format":"[{a..b}]","a":{"format":"x"}}`,
|
"double dot": `{"format":"[{a..b}]","a":"x"}`,
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, file := range rejected {
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||||
@@ -681,7 +667,7 @@ 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":{"format":"{..a}"}}}`,
|
"a": `{"format":"{p.x}","p":{"format":"{x}","x":"{..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)
|
||||||
|
|||||||
+27
-27
@@ -13,10 +13,10 @@ func TestBuiltinIDGenerators(t *testing.T) {
|
|||||||
tmpl string
|
tmpl string
|
||||||
re *regexp.Regexp
|
re *regexp.Regexp
|
||||||
}{
|
}{
|
||||||
{"uuid v7", `{"format":"{uuid()}"}`, regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)},
|
{"uuid v7", `"{uuid()}"`, regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)},
|
||||||
{"ulid", `{"format":"{ulid()}"}`, regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Z]{25}$`)},
|
{"ulid", `"{ulid()}"`, regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Z]{25}$`)},
|
||||||
{"nanoid", `{"format":"{nanoid(21)}"}`, regexp.MustCompile(`^[A-Za-z0-9_-]{21}$`)},
|
{"nanoid", `"{nanoid(21)}"`, regexp.MustCompile(`^[A-Za-z0-9_-]{21}$`)},
|
||||||
{"hex", `{"format":"{hex(16)}"}`, regexp.MustCompile(`^[0-9a-f]{16}$`)},
|
{"hex", `"{hex(16)}"`, regexp.MustCompile(`^[0-9a-f]{16}$`)},
|
||||||
}
|
}
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@@ -38,9 +38,9 @@ func TestBuiltinIDGenerators(t *testing.T) {
|
|||||||
// sample draws only from the seeded rng, so same seed -> same value.
|
// sample draws only from the seeded rng, so same seed -> same value.
|
||||||
func TestBuiltinSamplesReproducible(t *testing.T) {
|
func TestBuiltinSamplesReproducible(t *testing.T) {
|
||||||
for _, tmpl := range []string{
|
for _, tmpl := range []string{
|
||||||
`{"format":"{uuid()}"}`, `{"format":"{ulid()}"}`,
|
`"{uuid()}"`, `"{ulid()}"`,
|
||||||
`{"format":"{nanoid(12)}"}`, `{"format":"{int(1,1000000)}"}`,
|
`"{nanoid(12)}"`, `"{int(1,1000000)}"`,
|
||||||
`{"format":"{float(0,1,6)}"}`, `{"format":"{base64(12)}"}`, `{"format":"{iban(SE)}"}`,
|
`"{float(0,1,6)}"`, `"{base64(12)}"`, `"{iban(SE)}"`,
|
||||||
} {
|
} {
|
||||||
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
||||||
t.Fatalf("%s not reproducible: %q != %q", tmpl, a, b)
|
t.Fatalf("%s not reproducible: %q != %q", tmpl, a, b)
|
||||||
@@ -52,7 +52,7 @@ func TestBuiltinIntInclusiveRange(t *testing.T) {
|
|||||||
f := engine(1)
|
f := engine(1)
|
||||||
lo, hi := false, false
|
lo, hi := false, false
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
n, err := strconv.Atoi(mustRender(t, f, `{"format":"{int(3,7)}"}`))
|
n, err := strconv.Atoi(mustRender(t, f, `"{int(3,7)}"`))
|
||||||
if err != nil || n < 3 || n > 7 {
|
if err != nil || n < 3 || n > 7 {
|
||||||
t.Fatalf("int(3,7) = %d (err %v), out of range", n, err)
|
t.Fatalf("int(3,7) = %d (err %v), out of range", n, err)
|
||||||
}
|
}
|
||||||
@@ -66,14 +66,14 @@ func TestBuiltinIntInclusiveRange(t *testing.T) {
|
|||||||
func TestBuiltinFloat(t *testing.T) {
|
func TestBuiltinFloat(t *testing.T) {
|
||||||
f, re := engine(1), regexp.MustCompile(`^[12]\.\d{3}$`)
|
f, re := engine(1), regexp.MustCompile(`^[12]\.\d{3}$`)
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
if got := mustRender(t, f, `{"format":"{float(1,2,3)}"}`); !re.MatchString(got) {
|
if got := mustRender(t, f, `"{float(1,2,3)}"`); !re.MatchString(got) {
|
||||||
t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got)
|
t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuiltinBase64(t *testing.T) {
|
func TestBuiltinBase64(t *testing.T) {
|
||||||
got := mustRender(t, engine(1), `{"format":"{base64(9)}"}`)
|
got := mustRender(t, engine(1), `"{base64(9)}"`)
|
||||||
if b, err := base64.StdEncoding.DecodeString(got); err != nil || len(b) != 9 {
|
if b, err := base64.StdEncoding.DecodeString(got); err != nil || len(b) != 9 {
|
||||||
t.Fatalf("base64(9) = %q decodes to %d bytes (err %v), want 9", got, len(b), err)
|
t.Fatalf("base64(9) = %q decodes to %d bytes (err %v), want 9", got, len(b), err)
|
||||||
}
|
}
|
||||||
@@ -83,9 +83,9 @@ func TestBuiltinBase64(t *testing.T) {
|
|||||||
// hand-computed check, like the luhn test. mod-11 emits X when it would be 10.
|
// hand-computed check, like the luhn test. mod-11 emits X when it would be 10.
|
||||||
func TestBuiltinChecksums(t *testing.T) {
|
func TestBuiltinChecksums(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"12345678{mod11()}"}`: "123456785", // weights 2..7 from the right
|
`"12345678{mod11()}"`: "123456785", // weights 2..7 from the right
|
||||||
`{"format":"6{mod11()}"}`: "6X", // remainder 10 -> X
|
`"6{mod11()}"`: "6X", // remainder 10 -> X
|
||||||
`{"format":"400638133393{ean()}"}`: "4006381333931", // EAN-13 (= ISBN-13) check digit
|
`"400638133393{ean()}"`: "4006381333931", // EAN-13 (= ISBN-13) check digit
|
||||||
}
|
}
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
@@ -101,14 +101,14 @@ func TestBuiltinChecksums(t *testing.T) {
|
|||||||
func TestBuiltinSeqPerSession(t *testing.T) {
|
func TestBuiltinSeqPerSession(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for i := 1; i <= 5; i++ {
|
for i := 1; i <= 5; i++ {
|
||||||
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != strconv.Itoa(i) {
|
if got := mustRender(t, f, `"{seq()}"`); got != strconv.Itoa(i) {
|
||||||
t.Fatalf("seq call %d = %q, want %d", i, got, i)
|
t.Fatalf("seq call %d = %q, want %d", i, got, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if got := mustRender(t, f, `{"format":"{seq(orders)}"}`); got != "1" {
|
if got := mustRender(t, f, `"{seq(orders)}"`); got != "1" {
|
||||||
t.Fatalf("named seq(orders) = %q, want its own count from 1", got)
|
t.Fatalf("named seq(orders) = %q, want its own count from 1", got)
|
||||||
}
|
}
|
||||||
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != "6" {
|
if got := mustRender(t, f, `"{seq()}"`); got != "6" {
|
||||||
t.Fatalf("default seq after the named one = %q, want 6 (counters are independent)", got)
|
t.Fatalf("default seq after the named one = %q, want 6 (counters are independent)", got)
|
||||||
}
|
}
|
||||||
// repeat drives one counter forward across its renders...
|
// repeat drives one counter forward across its renders...
|
||||||
@@ -116,7 +116,7 @@ func TestBuiltinSeqPerSession(t *testing.T) {
|
|||||||
t.Fatalf("repeat seq = %q, want 1,2,3", got)
|
t.Fatalf("repeat seq = %q, want 1,2,3", got)
|
||||||
}
|
}
|
||||||
// ...and a fresh session restarts from 1.
|
// ...and a fresh session restarts from 1.
|
||||||
if got := mustRender(t, engine(2), `{"format":"{seq()}"}`); got != "1" {
|
if got := mustRender(t, engine(2), `"{seq()}"`); got != "1" {
|
||||||
t.Fatalf("new session seq = %q, want 1", got)
|
t.Fatalf("new session seq = %q, want 1", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,14 +126,14 @@ func TestBuiltinSeqPerSession(t *testing.T) {
|
|||||||
// at compile, not detonate when the template is drawn.
|
// at compile, not detonate when the template is drawn.
|
||||||
func TestBuiltinArgLimits(t *testing.T) {
|
func TestBuiltinArgLimits(t *testing.T) {
|
||||||
bad := []string{
|
bad := []string{
|
||||||
`{"format":"{int(-9223372036854775808,9223372036854775807)}"}`, // span overflows int -> IntN panic
|
`"{int(-9223372036854775808,9223372036854775807)}"`, // span overflows int -> IntN panic
|
||||||
`{"format":"{hex(2000000000)}"}`, // ~2 GB string
|
`"{hex(2000000000)}"`, // ~2 GB string
|
||||||
`{"format":"{nanoid(2000000000)}"}`,
|
`"{nanoid(2000000000)}"`,
|
||||||
`{"format":"{base64(2000000000)}"}`,
|
`"{base64(2000000000)}"`,
|
||||||
`{"format":"{float(-1e308,1e308,2)}"}`, // span is +Inf
|
`"{float(-1e308,1e308,2)}"`, // span is +Inf
|
||||||
`{"format":"{float(0,1,100000)}"}`, // decimals -> huge string
|
`"{float(0,1,100000)}"`, // decimals -> huge string
|
||||||
`{"format":"{calc(1+1,100000)}"}`, // calc decimals -> huge string
|
`"{calc(1+1,100000)}"`, // calc decimals -> huge string
|
||||||
`{"format":"{x}","x":["1"],"repeat":2000000000}`, // repeat -> multi-GB join
|
`{"format":"{x}","x":"1","repeat":2000000000}`, // repeat -> multi-GB join
|
||||||
}
|
}
|
||||||
for _, tmpl := range bad {
|
for _, tmpl := range bad {
|
||||||
if _, err := compile(parse(t, tmpl)); err == nil {
|
if _, err := compile(parse(t, tmpl)); err == nil {
|
||||||
@@ -141,7 +141,7 @@ func TestBuiltinArgLimits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ordinary values still compile.
|
// Ordinary values still compile.
|
||||||
if _, err := compile(parse(t, `{"format":"{hex(16)} {int(-5,5)} {float(0,1,3)} {calc(1+1,2)}"}`)); err != nil {
|
if _, err := compile(parse(t, `"{hex(16)} {int(-5,5)} {float(0,1,3)} {calc(1+1,2)}"`)); err != nil {
|
||||||
t.Errorf("compile of normal args failed: %v", err)
|
t.Errorf("compile of normal args failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func TestBuiltinIBAN(t *testing.T) {
|
|||||||
wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15}
|
wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15}
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for cc, n := range wantLen {
|
for cc, n := range wantLen {
|
||||||
tmpl := `{"format":"{iban(` + cc + `)}"}`
|
tmpl := `"{iban(` + cc + `)}"`
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
got := mustRender(t, f, tmpl)
|
got := mustRender(t, f, tmpl)
|
||||||
if got[:2] != cc || len(got) != n {
|
if got[:2] != cc || len(got) != n {
|
||||||
|
|||||||
+18
-18
@@ -12,14 +12,14 @@ import (
|
|||||||
func TestCalcArithmetic(t *testing.T) {
|
func TestCalcArithmetic(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"{calc(2 + 3)}"}`: "5",
|
`"{calc(2 + 3)}"`: "5",
|
||||||
`{"format":"{calc(2 * 3 + 4)}"}`: "10", // * binds tighter than +
|
`"{calc(2 * 3 + 4)}"`: "10", // * binds tighter than +
|
||||||
`{"format":"{calc(2 + 3 * 4)}"}`: "14",
|
`"{calc(2 + 3 * 4)}"`: "14",
|
||||||
`{"format":"{calc((2 + 3) * 4)}"}`: "20", // parentheses override
|
`"{calc((2 + 3) * 4)}"`: "20", // parentheses override
|
||||||
`{"format":"{calc(10 / 4)}"}`: "2.5",
|
`"{calc(10 / 4)}"`: "2.5",
|
||||||
`{"format":"{calc(-2 + 5)}"}`: "3", // unary minus
|
`"{calc(-2 + 5)}"`: "3", // unary minus
|
||||||
`{"format":"{calc(2 - -3)}"}`: "5",
|
`"{calc(2 - -3)}"`: "5",
|
||||||
`{"format":"{calc(1.5 * 2)}"}`: "3", // whole result drops the decimals
|
`"{calc(1.5 * 2)}"`: "3", // whole result drops the decimals
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -33,9 +33,9 @@ func TestCalcArithmetic(t *testing.T) {
|
|||||||
func TestCalcAuto(t *testing.T) {
|
func TestCalcAuto(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"{calc(10 / 3)}"}`: "3.3333333333333335",
|
`"{calc(10 / 3)}"`: "3.3333333333333335",
|
||||||
`{"format":"{calc(6 / 2)}"}`: "3",
|
`"{calc(6 / 2)}"`: "3",
|
||||||
`{"format":"{calc(1 / 4)}"}`: "0.25",
|
`"{calc(1 / 4)}"`: "0.25",
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -49,9 +49,9 @@ func TestCalcAuto(t *testing.T) {
|
|||||||
func TestCalcDecimals(t *testing.T) {
|
func TestCalcDecimals(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"{calc(10 / 3, 2)}"}`: "3.33",
|
`"{calc(10 / 3, 2)}"`: "3.33",
|
||||||
`{"format":"{calc(10 / 3, 0)}"}`: "3",
|
`"{calc(10 / 3, 0)}"`: "3",
|
||||||
`{"format":"{calc(2 * 3, 2)}"}`: "6.00",
|
`"{calc(2 * 3, 2)}"`: "6.00",
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -63,11 +63,11 @@ func TestCalcDecimals(t *testing.T) {
|
|||||||
// TestCalcFields pins that bare names resolve to sibling fields, rendered then
|
// TestCalcFields pins that bare names resolve to sibling fields, rendered then
|
||||||
// parsed as numbers.
|
// parsed as numbers.
|
||||||
func TestCalcFields(t *testing.T) {
|
func TestCalcFields(t *testing.T) {
|
||||||
if got := mustRender(t, engine(1), `{"format":"{calc(price * qty, 2)}","price":["19.99"],"qty":["3"]}`); got != "59.97" {
|
if got := mustRender(t, engine(1), `{"format":"{calc(price * qty, 2)}","price":"19.99","qty":"3"}`); got != "59.97" {
|
||||||
t.Fatalf("calc over fields = %q, want 59.97", got)
|
t.Fatalf("calc over fields = %q, want 59.97", got)
|
||||||
}
|
}
|
||||||
// A field that is itself a template renders before parsing.
|
// A field that is itself a template renders before parsing.
|
||||||
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":[{"format":"{n}","n":["10"]}],"b":["3"]}`); got != "7" {
|
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":{"format":"{n}","n":"10"},"b":"3"}`); got != "7" {
|
||||||
t.Fatalf("calc(a - b) = %q, want 7", got)
|
t.Fatalf("calc(a - b) = %q, want 7", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,14 +75,14 @@ func TestCalcFields(t *testing.T) {
|
|||||||
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that doesn't render
|
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that doesn't render
|
||||||
// to a number becomes NaN, which propagates and prints visibly.
|
// to a number becomes NaN, which propagates and prints visibly.
|
||||||
func TestCalcNonNumericIsNaN(t *testing.T) {
|
func TestCalcNonNumericIsNaN(t *testing.T) {
|
||||||
if got := mustRender(t, engine(1), `{"format":"{calc(x * 2)}","x":["abc"]}`); got != "NaN" {
|
if got := mustRender(t, engine(1), `{"format":"{calc(x * 2)}","x":"abc"}`); got != "NaN" {
|
||||||
t.Fatalf("calc over non-numeric field = %q, want NaN", got)
|
t.Fatalf("calc over non-numeric field = %q, want NaN", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCalcReproducible pins that a calc over a random operand stays seed-stable.
|
// TestCalcReproducible pins that a calc over a random operand stays seed-stable.
|
||||||
func TestCalcReproducible(t *testing.T) {
|
func TestCalcReproducible(t *testing.T) {
|
||||||
tmpl := `{"format":"{calc(q * 2 + 1)}","q":[{"format":"{int(1,1000000)}"}]}`
|
tmpl := `{"format":"{calc(q * 2 + 1)}","q":"{int(1,1000000)}"}`
|
||||||
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
||||||
t.Fatalf("calc not reproducible: %q != %q", a, b)
|
t.Fatalf("calc not reproducible: %q != %q", a, b)
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-41
@@ -16,11 +16,11 @@ import (
|
|||||||
func TestHashIsLiteral(t *testing.T) {
|
func TestHashIsLiteral(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"","x":["v"]}`: "",
|
`{"format":"","x":"v"}`: "",
|
||||||
`{"format":"#","x":["v"]}`: "#",
|
`{"format":"#","x":"v"}`: "#",
|
||||||
`{"format":"##","x":["v"]}`: "##",
|
`{"format":"##","x":"v"}`: "##",
|
||||||
`{"format":"#0#1#A#a","x":["v"]}`: "#0#1#A#a",
|
`{"format":"#0#1#A#a","x":"v"}`: "#0#1#A#a",
|
||||||
`{"format":"#{x}","x":["v"]}`: "#v",
|
`{"format":"#{x}","x":"v"}`: "#v",
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -32,7 +32,7 @@ func TestHashIsLiteral(t *testing.T) {
|
|||||||
func TestMultibyteFormat(t *testing.T) {
|
func TestMultibyteFormat(t *testing.T) {
|
||||||
// Scanning is rune-aware: multibyte literals coexist with class chars and
|
// Scanning is rune-aware: multibyte literals coexist with class chars and
|
||||||
// tokens without corrupting indices.
|
// tokens without corrupting indices.
|
||||||
got := mustRender(t, engine(2), `{"format":"Öster{x}-{digits(1)}å","x":["väg"]}`)
|
got := mustRender(t, engine(2), `{"format":"Öster{x}-{digits(1)}å","x":"väg"}`)
|
||||||
if !regexp.MustCompile(`^Österväg-[0-9]å$`).MatchString(got) {
|
if !regexp.MustCompile(`^Österväg-[0-9]å$`).MatchString(got) {
|
||||||
t.Fatalf("multibyte format = %q", got)
|
t.Fatalf("multibyte format = %q", got)
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ func TestAlternationThreeWay(t *testing.T) {
|
|||||||
f := engine(4)
|
f := engine(4)
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
seen[mustRender(t, f, `{"format":"{a|b|c}","a":["A"],"b":["B"],"c":["C"]}`)] = true
|
seen[mustRender(t, f, `{"format":"{a|b|c}","a":"A","b":"B","c":"C"}`)] = true
|
||||||
}
|
}
|
||||||
if !seen["A"] || !seen["B"] || !seen["C"] || len(seen) != 3 {
|
if !seen["A"] || !seen["B"] || !seen["C"] || len(seen) != 3 {
|
||||||
t.Fatalf("3-way alternation produced %v, want A, B and C", seen)
|
t.Fatalf("3-way alternation produced %v, want A, B and C", seen)
|
||||||
@@ -69,11 +69,11 @@ func TestNewErrors(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
"separator without repeat": {
|
"separator without repeat": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"separator":","}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","separator":","}`},
|
||||||
"has no effect without a repeat above 1",
|
"has no effect without a repeat above 1",
|
||||||
},
|
},
|
||||||
"separator with an explicit repeat of 1": {
|
"separator with an explicit repeat of 1": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"repeat":1,"separator":","}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","separator":","}`},
|
||||||
"has no effect without a repeat above 1",
|
"has no effect without a repeat above 1",
|
||||||
},
|
},
|
||||||
"weight outside a choice": {
|
"weight outside a choice": {
|
||||||
@@ -85,38 +85,38 @@ func TestNewErrors(t *testing.T) {
|
|||||||
"weight only skews a choice's items",
|
"weight only skews a choice's items",
|
||||||
},
|
},
|
||||||
"weight used as a field": {
|
"weight used as a field": {
|
||||||
map[string]string{"a": `{"format":"{name} {weight}kg","name":["Anvil"],"weight":["7"]}`},
|
map[string]string{"a": `{"format":"{name} {weight}kg","name":"Anvil","weight":["7"]}`},
|
||||||
"can never be a field",
|
"can never be a field",
|
||||||
},
|
},
|
||||||
"an option name used as a token": {
|
"an option name used as a token": {
|
||||||
map[string]string{"a": `{"format":"{weight}"}`},
|
map[string]string{"a": `"{weight}"`},
|
||||||
`"weight" is an option and can never be a field`,
|
`"weight" is an option and can never be a field`,
|
||||||
},
|
},
|
||||||
"category name with a dot": {
|
"category name with a dot": {
|
||||||
map[string]string{"a.b": `["1"]`},
|
map[string]string{"a.b": `"1"`},
|
||||||
`category "a.b" contains "."`,
|
`category "a.b" contains "."`,
|
||||||
},
|
},
|
||||||
"folder name with a dot": {
|
"folder name with a dot": {
|
||||||
map[string]string{"a.b/cat": `["1"]`},
|
map[string]string{"a.b/cat": `"1"`},
|
||||||
`/a.b: folder "a.b" contains "."`,
|
`/a.b: folder "a.b" contains "."`,
|
||||||
},
|
},
|
||||||
// The token grammar reserves three more characters. A name carrying one
|
// The token grammar reserves three more characters. A name carrying one
|
||||||
// still resolves by dot path, but no format can name it, so it is rejected
|
// still resolves by dot path, but no format can name it, so it is rejected
|
||||||
// where it is authored rather than at the token that cannot reach it.
|
// where it is authored rather than at the token that cannot reach it.
|
||||||
"field name with a pipe": {
|
"field name with a pipe": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"b|c":["2"]}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","b|c":"2"}`},
|
||||||
`field "b|c" contains "|"`,
|
`field "b|c" contains "|"`,
|
||||||
},
|
},
|
||||||
"field name with a paren": {
|
"field name with a paren": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"b(c":["2"]}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","b(c":"2"}`},
|
||||||
`field "b(c" contains "("`,
|
`field "b(c" contains "("`,
|
||||||
},
|
},
|
||||||
"field name with a closing brace": {
|
"field name with a closing brace": {
|
||||||
map[string]string{"a": `{"format":"{x}","x":["1"],"b}c":["2"]}`},
|
map[string]string{"a": `{"format":"{x}","x":"1","b}c":"2"}`},
|
||||||
`field "b}c" contains "}"`,
|
`field "b}c" contains "}"`,
|
||||||
},
|
},
|
||||||
"category name with a pipe": {
|
"category name with a pipe": {
|
||||||
map[string]string{"a|b": `["1"]`},
|
map[string]string{"a|b": `"1"`},
|
||||||
`category "a|b" contains "|"`,
|
`category "a|b" contains "|"`,
|
||||||
},
|
},
|
||||||
// An empty name is not a path segment, so List never offered it — while a
|
// An empty name is not a path segment, so List never offered it — while a
|
||||||
@@ -127,42 +127,42 @@ func TestNewErrors(t *testing.T) {
|
|||||||
`field "" is empty`,
|
`field "" is empty`,
|
||||||
},
|
},
|
||||||
"folder name with a paren": {
|
"folder name with a paren": {
|
||||||
map[string]string{"a(b/cat": `["1"]`},
|
map[string]string{"a(b/cat": `"1"`},
|
||||||
`folder "a(b" contains "("`,
|
`folder "a(b" contains "("`,
|
||||||
},
|
},
|
||||||
// A repeated arm skews an alternation, which weight is the spelling for.
|
// A repeated arm skews an alternation, which weight is the spelling for.
|
||||||
"repeated alternation arm": {
|
"repeated alternation arm": {
|
||||||
map[string]string{"a": `{"format":"{x|x}","x":["1"]}`},
|
map[string]string{"a": `{"format":"{x|x}","x":"1"}`},
|
||||||
`arm "x" is repeated`,
|
`arm "x" is repeated`,
|
||||||
},
|
},
|
||||||
"repeated arm among others": {
|
"repeated arm among others": {
|
||||||
map[string]string{"a": `{"format":"{x|y|x}","x":["1"],"y":["2"]}`},
|
map[string]string{"a": `{"format":"{x|y|x}","x":"1","y":"2"}`},
|
||||||
`arm "x" is repeated`,
|
`arm "x" is repeated`,
|
||||||
},
|
},
|
||||||
"repeated path arm": {
|
"repeated path arm": {
|
||||||
map[string]string{"a": `{"format":"{p.v|p.v}","p":{"format":"{v}","v":["1"]}}`},
|
map[string]string{"a": `{"format":"{p.v|p.v}","p":{"format":"{v}","v":"1"}}`},
|
||||||
`arm "p.v" is repeated`,
|
`arm "p.v" is repeated`,
|
||||||
},
|
},
|
||||||
// A reference arm is the only kind that reaches the repeat check by passing
|
// A reference arm is the only kind that reaches the repeat check by passing
|
||||||
// the per-arm checks rather than falling through them.
|
// the per-arm checks rather than falling through them.
|
||||||
"repeated reference arm": {
|
"repeated reference arm": {
|
||||||
map[string]string{"a": `["x"]`, "b": `{"format":"{..a|..a}"}`},
|
map[string]string{"a": `"x"`, "b": `"{..a|..a}"`},
|
||||||
`arm "..a" is repeated`,
|
`arm "..a" is repeated`,
|
||||||
},
|
},
|
||||||
// An arm that is broken on its own terms is reported as that, not as a
|
// An arm that is broken on its own terms is reported as that, not as a
|
||||||
// repeat: the repeat is a consequence of the real mistake.
|
// repeat: the repeat is a consequence of the real mistake.
|
||||||
"repeated arm with no path": {
|
"repeated arm with no path": {
|
||||||
map[string]string{"a": `{"format":"{..|..}"}`},
|
map[string]string{"a": `"{..|..}"`},
|
||||||
"reference has no path",
|
"reference has no path",
|
||||||
},
|
},
|
||||||
// No field can be named "", so the token is told that rather than sent to
|
// No field can be named "", so the token is told that rather than sent to
|
||||||
// name one — the fix "no field" points at is itself a load error.
|
// name one — the fix "no field" points at is itself a load error.
|
||||||
"repeated empty arm": {
|
"repeated empty arm": {
|
||||||
map[string]string{"a": `{"format":"{|}"}`},
|
map[string]string{"a": `"{|}"`},
|
||||||
"a name is never empty",
|
"a name is never empty",
|
||||||
},
|
},
|
||||||
"bare empty token": {
|
"bare empty token": {
|
||||||
map[string]string{"a": `{"format":"[{}]","x":["1"]}`},
|
map[string]string{"a": `{"format":"[{}]","x":"1"}`},
|
||||||
"a name is never empty",
|
"a name is never empty",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -182,16 +182,16 @@ func TestNewErrors(t *testing.T) {
|
|||||||
path string
|
path string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
"option name as a field": {map[string]string{"a": `{"format":"{name} {Weight}kg","name":["Anvil"],"Weight":["7"]}`}, "a", "Anvil 7kg"},
|
"option name as a field": {map[string]string{"a": `{"format":"{name} {Weight}kg","name":"Anvil","Weight":"7"}`}, "a", "Anvil 7kg"},
|
||||||
"format spelling as a field": {map[string]string{"a": `{"format":"{Format}","Format":["PDF"]}`}, "a", "PDF"},
|
"format spelling as a field": {map[string]string{"a": `{"format":"{Format}","Format":"PDF"}`}, "a", "PDF"},
|
||||||
"hyphenated field": {map[string]string{"a": `{"format":"{x-y}","x-y":["1"]}`}, "a.x-y", "1"},
|
"hyphenated field": {map[string]string{"a": `{"format":"{x-y}","x-y":"1"}`}, "a.x-y", "1"},
|
||||||
"category named Format": {map[string]string{"Format": `["1"]`}, "Format", "1"},
|
"category named Format": {map[string]string{"Format": `"1"`}, "Format", "1"},
|
||||||
"folder named Repeat": {map[string]string{"Repeat/cat": `["1"]`}, "Repeat.cat", "1"},
|
"folder named Repeat": {map[string]string{"Repeat/cat": `"1"`}, "Repeat.cat", "1"},
|
||||||
"field with a closing paren": {map[string]string{"a": `{"format":"{b)c}","b)c":["2"]}`}, "a", "2"},
|
"field with a closing paren": {map[string]string{"a": `{"format":"{b)c}","b)c":"2"}`}, "a", "2"},
|
||||||
"repeat without a separator": {map[string]string{"a": `{"format":"{x}","repeat":3,"x":["1"]}`}, "a", "111"},
|
"repeat without a separator": {map[string]string{"a": `{"format":"{x}","repeat":3,"x":"1"}`}, "a", "111"},
|
||||||
// One name in two separate tokens is two independent draws, not a repeated
|
// One name in two separate tokens is two independent draws, not a repeated
|
||||||
// arm; only a repeat within one alternation is rejected.
|
// arm; only a repeat within one alternation is rejected.
|
||||||
"one name in two tokens": {map[string]string{"a": `{"format":"{x}{x}","x":["1"]}`}, "a", "11"},
|
"one name in two tokens": {map[string]string{"a": `{"format":"{x}{x}","x":"1"}`}, "a", "11"},
|
||||||
}
|
}
|
||||||
for name, c := range accepted {
|
for name, c := range accepted {
|
||||||
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files)))
|
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files)))
|
||||||
@@ -212,7 +212,7 @@ func TestDeepDottedPath(t *testing.T) {
|
|||||||
// on the path are single-variant, so it resolves deterministically.
|
// on the path are single-variant, so it resolves deterministically.
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
f.categories = map[string]node{
|
f.categories = map[string]node{
|
||||||
"deep": compiled(t, `[{"format":"{a}","a":[{"format":"{b}","b":[{"format":"{c}","c":[{"format":"{d}","d":["leaf"]}]}]}]}]`),
|
"deep": compiled(t, `{"format":"{a}","a":{"format":"{b}","b":{"format":"{c}","c":{"format":"{d}","d":"leaf"}}}}`),
|
||||||
}
|
}
|
||||||
if got, err := f.Fake("deep.a.b.c.d"); err != nil || got != "leaf" {
|
if got, err := f.Fake("deep.a.b.c.d"); err != nil || got != "leaf" {
|
||||||
t.Fatalf("Fake(deep.a.b.c.d) = %q, %v, want leaf", got, err)
|
t.Fatalf("Fake(deep.a.b.c.d) = %q, %v, want leaf", got, err)
|
||||||
@@ -225,7 +225,7 @@ func TestDeepDottedPath(t *testing.T) {
|
|||||||
|
|
||||||
func TestDescendIntoStringErrors(t *testing.T) {
|
func TestDescendIntoStringErrors(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
f.categories = map[string]node{"greeting": compiled(t, `["hej"]`)}
|
f.categories = map[string]node{"greeting": compiled(t, `"hej"`)}
|
||||||
if _, err := f.Fake("greeting.extra"); err == nil || !strings.Contains(err.Error(), `no field "extra"`) {
|
if _, err := f.Fake("greeting.extra"); err == nil || !strings.Contains(err.Error(), `no field "extra"`) {
|
||||||
t.Fatalf("Fake(greeting.extra) = %v, want a no-field error", err)
|
t.Fatalf("Fake(greeting.extra) = %v, want a no-field error", err)
|
||||||
}
|
}
|
||||||
@@ -235,9 +235,9 @@ func TestDescendIntoStringErrors(t *testing.T) {
|
|||||||
// one call and failing on the next: every variant must carry the rest of the path.
|
// one call and failing on the next: every variant must carry the rest of the path.
|
||||||
func TestPathThroughChoice(t *testing.T) {
|
func TestPathThroughChoice(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"every": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"]}]`,
|
"every": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`,
|
||||||
"notall": `[{"format":"{f}","f":["1"]},["plain"]]`,
|
"notall": `[{"format":"{f}","f":"1"},"plain"]`,
|
||||||
"some": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"],"extra":["x"]}]`,
|
"some": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2","extra":"x"}]`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
@@ -269,7 +269,7 @@ func TestPathThroughChoice(t *testing.T) {
|
|||||||
// wherever it appears.
|
// wherever it appears.
|
||||||
func TestPathKeyIsUnambiguous(t *testing.T) {
|
func TestPathKeyIsUnambiguous(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"cat": `[{"format":"{a.b}","a.b":["1"]},{"format":"{a}","a":{"format":"{b}","b":["2"]}}]`,
|
"cat": `[{"format":"{a.b}","a.b":"1"},{"format":"{a}","a":{"format":"{b}","b":"2"}}]`,
|
||||||
})
|
})
|
||||||
_, err := New(WithoutShippedData(), WithDataPath(dir))
|
_, err := New(WithoutShippedData(), WithDataPath(dir))
|
||||||
if err == nil || !strings.Contains(err.Error(), `field "a.b" contains "."`) {
|
if err == nil || !strings.Contains(err.Error(), `field "a.b" contains "."`) {
|
||||||
@@ -277,7 +277,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// The same data without the dotted key is fine, and the path resolves.
|
// The same data without the dotted key is fine, and the path resolves.
|
||||||
f := newGenerator(t, writeData(t, map[string]string{
|
f := newGenerator(t, writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{a.b}","a":{"format":"{b}","b":["2"]}}`,
|
"cat": `{"format":"{a.b}","a":{"format":"{b}","b":"2"}}`,
|
||||||
}), WithSeed(1))
|
}), WithSeed(1))
|
||||||
if !slices.Contains(f.List(), "cat.a.b") {
|
if !slices.Contains(f.List(), "cat.a.b") {
|
||||||
t.Error("List() omits cat.a.b, which the data carries")
|
t.Error("List() omits cat.a.b, which the data carries")
|
||||||
@@ -302,7 +302,7 @@ func TestMissingFieldNamesItself(t *testing.T) {
|
|||||||
|
|
||||||
func TestCategoryRootShapes(t *testing.T) {
|
func TestCategoryRootShapes(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"obj": `{"format":"{digits(2)}"}`, // object root
|
"obj": `"{digits(2)}"`, // object root
|
||||||
"lit": `"hello"`, // bare-string root
|
"lit": `"hello"`, // bare-string root
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
|
|||||||
+16
-16
@@ -13,13 +13,13 @@ import (
|
|||||||
// folder segments — descending transparently through single-variant choices.
|
// folder segments — descending transparently through single-variant choices.
|
||||||
func TestList(t *testing.T) {
|
func TestList(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"person": `{"format":"{first} {last}","first":["A"],"last":["B"]}`,
|
"person": `{"format":"{first} {last}","first":"A","last":"B"}`,
|
||||||
"word": `["x", "y"]`,
|
"word": `["x", "y"]`,
|
||||||
"geo/city": `["Z"]`,
|
"geo/city": `"Z"`,
|
||||||
// A bound {..path} reference is a render edge, not an addressable field.
|
// A bound {..path} reference is a render edge, not an addressable field.
|
||||||
"greeting": `{"format":"hej {..person.first} and {own}","own":["x"]}`,
|
"greeting": `{"format":"hej {..person.first} and {own}","own":"x"}`,
|
||||||
// Only the fields every variant carries are addressable, so "extra" is not.
|
// Only the fields every variant carries are addressable, so "extra" is not.
|
||||||
"coin": `[{"format":"{code}","code":["A"],"name":["Aa"]},{"format":"{code}","code":["B"],"name":["Bb"],"extra":["x"]}]`,
|
"coin": `[{"format":"{code}","code":"A","name":"Aa"},{"format":"{code}","code":"B","name":"Bb","extra":"x"}]`,
|
||||||
})
|
})
|
||||||
got := newGenerator(t, dir, WithSeed(1)).List()
|
got := newGenerator(t, dir, WithSeed(1)).List()
|
||||||
want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"}
|
want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"}
|
||||||
@@ -66,8 +66,8 @@ func TestNewEmptyDirErrors(t *testing.T) {
|
|||||||
// namespace, so data/<loc>/person.json is reachable as "<loc>.person".
|
// namespace, so data/<loc>/person.json is reachable as "<loc>.person".
|
||||||
func TestFoldersBecomeDotPaths(t *testing.T) {
|
func TestFoldersBecomeDotPaths(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"sv_SE/greeting": `["hej"]`,
|
"sv_SE/greeting": `"hej"`,
|
||||||
"en_US/greeting": `["hi"]`,
|
"en_US/greeting": `"hi"`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.greeting"); got != "hej" {
|
if got := fake(t, f, "sv_SE.greeting"); got != "hej" {
|
||||||
@@ -82,7 +82,7 @@ func TestFoldersBecomeDotPaths(t *testing.T) {
|
|||||||
// a/b/c, then deep JSON fields inside the file at the end of that path.
|
// a/b/c, then deep JSON fields inside the file at the end of that path.
|
||||||
func TestNestedFoldersAndJSON(t *testing.T) {
|
func TestNestedFoldersAndJSON(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":["leaf"]}}}`,
|
"a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":"leaf"}}}`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
// Folders a.b.c, file thing, then JSON fields x.y.z — one continuous path.
|
// Folders a.b.c, file thing, then JSON fields x.y.z — one continuous path.
|
||||||
@@ -96,7 +96,7 @@ func TestNestedFoldersAndJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderingAFolderErrors(t *testing.T) {
|
func TestRenderingAFolderErrors(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"sv_SE/greeting": `["hej"]`})
|
dir := writeData(t, map[string]string{"sv_SE/greeting": `"hej"`})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if _, err := f.Fake("sv_SE"); err == nil {
|
if _, err := f.Fake("sv_SE"); err == nil {
|
||||||
t.Fatal("Fake(folder) = nil error, want a not-a-value error")
|
t.Fatal("Fake(folder) = nil error, want a not-a-value error")
|
||||||
@@ -106,8 +106,8 @@ func TestRenderingAFolderErrors(t *testing.T) {
|
|||||||
// TestMultiPathLastWins loads two dirs; on a name clash the later dir wins, and
|
// TestMultiPathLastWins loads two dirs; on a name clash the later dir wins, and
|
||||||
// non-clashing entries from both are reachable (data combines).
|
// non-clashing entries from both are reachable (data combines).
|
||||||
func TestMultiPathLastWins(t *testing.T) {
|
func TestMultiPathLastWins(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"greeting": `["from-a"]`, "only-a": `["a"]`})
|
a := writeData(t, map[string]string{"greeting": `"from-a"`, "only-a": `"a"`})
|
||||||
b := writeData(t, map[string]string{"greeting": `["from-b"]`, "only-b": `["b"]`})
|
b := writeData(t, map[string]string{"greeting": `"from-b"`, "only-b": `"b"`})
|
||||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "greeting"); got != "from-b" {
|
if got := fake(t, f, "greeting"); got != "from-b" {
|
||||||
t.Fatalf("greeting = %q, want from-b (last loaded wins)", got)
|
t.Fatalf("greeting = %q, want from-b (last loaded wins)", got)
|
||||||
@@ -124,8 +124,8 @@ func TestMultiPathLastWins(t *testing.T) {
|
|||||||
// children rather than replacing wholesale: each dir adds a file to sv_SE, and
|
// children rather than replacing wholesale: each dir adds a file to sv_SE, and
|
||||||
// a per-file clash inside still resolves last-wins.
|
// a per-file clash inside still resolves last-wins.
|
||||||
func TestMultiPathMergesFolders(t *testing.T) {
|
func TestMultiPathMergesFolders(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"sv_SE/person": `["from-a"]`, "sv_SE/shared": `["a"]`})
|
a := writeData(t, map[string]string{"sv_SE/person": `"from-a"`, "sv_SE/shared": `"a"`})
|
||||||
b := writeData(t, map[string]string{"sv_SE/company": `["from-b"]`, "sv_SE/shared": `["b"]`})
|
b := writeData(t, map[string]string{"sv_SE/company": `"from-b"`, "sv_SE/shared": `"b"`})
|
||||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.person"); got != "from-a" {
|
if got := fake(t, f, "sv_SE.person"); got != "from-a" {
|
||||||
t.Fatalf("sv_SE.person = %q, want from-a (folder merged, not replaced)", got)
|
t.Fatalf("sv_SE.person = %q, want from-a (folder merged, not replaced)", got)
|
||||||
@@ -163,10 +163,10 @@ func TestListedPathsAllRender(t *testing.T) {
|
|||||||
// carrying no JSON never contributed a namespace, so neither may fail the load.
|
// carrying no JSON never contributed a namespace, so neither may fail the load.
|
||||||
func TestHiddenEntriesAreSkipped(t *testing.T) {
|
func TestHiddenEntriesAreSkipped(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"cat": `["V"]`,
|
"cat": `"V"`,
|
||||||
".git/HEAD": `["ignored"]`,
|
".git/HEAD": `"ignored"`,
|
||||||
".hidden": `["ignored"]`,
|
".hidden": `"ignored"`,
|
||||||
"empty.folder/doc": `["ignored"]`,
|
"empty.folder/doc": `"ignored"`,
|
||||||
})
|
})
|
||||||
if err := os.Rename(filepath.Join(dir, "empty.folder", "doc.json"), filepath.Join(dir, "empty.folder", "doc.txt")); err != nil {
|
if err := os.Rename(filepath.Join(dir, "empty.folder", "doc.json"), filepath.Join(dir, "empty.folder", "doc.txt")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOneItemChoiceIsRejected(t *testing.T) {
|
||||||
|
for _, src := range []string{`["x"]`, `[{"format":"{d}","d":"x"}]`, `{"format":"{w}","w":["only"]}`} {
|
||||||
|
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), "one-item choice") {
|
||||||
|
t.Errorf("compile(%s) = %v, want the one-item choice rejected", src, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatedChoiceItemIsRejected(t *testing.T) {
|
||||||
|
for src, want := range map[string]string{
|
||||||
|
`["a", "a", "b"]`: `{ "format": "a", "weight": 2 }`,
|
||||||
|
`[{"format":"{x}","x":"1"},{"format":"{x}","x":"1"}]`: "repeats item",
|
||||||
|
`{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`,
|
||||||
|
} {
|
||||||
|
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := compile(parse(t, `[{"format":"a","weight":2}, "b"]`)); err != nil {
|
||||||
|
t.Errorf("compile(weighted a, b) = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInertObjectIsRejected(t *testing.T) {
|
||||||
|
for src, want := range map[string]string{
|
||||||
|
`{"format":"Malmö"}`: `write "Malmö"`,
|
||||||
|
`{"format":"{digits(3)}"}`: `write "{digits(3)}"`,
|
||||||
|
`[{"format":"a","weight":1},"b"]`: "weight 1",
|
||||||
|
`{"format":"{x}","x":"v","repeat":1}`: "repeat 1",
|
||||||
|
`{"format":"{x}","x":"v","separator":","}`: "separator",
|
||||||
|
} {
|
||||||
|
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ok := range []string{`[{"format":"a","weight":2},"b"]`, `{"format":"ab","repeat":2}`, `{"format":"{x}","x":"v"}`, `"{digits(3)}"`} {
|
||||||
|
if _, err := compile(parse(t, ok)); err != nil {
|
||||||
|
t.Errorf("compile(%s) = %v", ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-37
@@ -6,8 +6,8 @@ import "testing"
|
|||||||
// pulls a value from another via a {..path} reference resolved from the data root.
|
// pulls a value from another via a {..path} reference resolved from the data root.
|
||||||
func TestRootReferenceAcrossFolders(t *testing.T) {
|
func TestRootReferenceAcrossFolders(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"en_US/person": `["Pat Smith"]`,
|
"en_US/person": `"Pat Smith"`,
|
||||||
"sv_SE/greeting": `{"format":"Hej, {..en_US.person}!"}`,
|
"sv_SE/greeting": `"Hej, {..en_US.person}!"`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
|
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
|
||||||
@@ -19,8 +19,8 @@ func TestRootReferenceAcrossFolders(t *testing.T) {
|
|||||||
// a single-variant choice and then a template field (..who.last).
|
// a single-variant choice and then a template field (..who.last).
|
||||||
func TestReferenceIntoAField(t *testing.T) {
|
func TestReferenceIntoAField(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"who": `[{"format":"{first} {last}","first":["Ada"],"last":["Byron"]}]`,
|
"who": `{"format":"{first} {last}","first":"Ada","last":"Byron"}`,
|
||||||
"card": `{"format":"signed {..who.last}"}`,
|
"card": `"signed {..who.last}"`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "card"); got != "signed Byron" {
|
if got := fake(t, f, "card"); got != "signed Byron" {
|
||||||
@@ -32,8 +32,8 @@ func TestReferenceIntoAField(t *testing.T) {
|
|||||||
// alternation, so a field and a cross-file value share one slot.
|
// alternation, so a field and a cross-file value share one slot.
|
||||||
func TestReferenceInAlternation(t *testing.T) {
|
func TestReferenceInAlternation(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"far": `["X"]`,
|
"far": `"X"`,
|
||||||
"near": `{"format":"{here|..far}","here":["H"]}`,
|
"near": `{"format":"{here|..far}","here":"H"}`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(2))
|
f := newGenerator(t, dir, WithSeed(2))
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
@@ -48,8 +48,8 @@ func TestReferenceInAlternation(t *testing.T) {
|
|||||||
// TestReferenceCombinesLoadedPaths is the point of references over the merge
|
// TestReferenceCombinesLoadedPaths is the point of references over the merge
|
||||||
// model: data layered from two dirs can point at each other through the root.
|
// model: data layered from two dirs can point at each other through the root.
|
||||||
func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"en_US/word": `["river"]`})
|
a := writeData(t, map[string]string{"en_US/word": `"river"`})
|
||||||
b := writeData(t, map[string]string{"mine/slug": `{"format":"the-{..en_US.word}"}`})
|
b := writeData(t, map[string]string{"mine/slug": `"the-{..en_US.word}"`})
|
||||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "mine.slug"); got != "the-river" {
|
if got := fake(t, f, "mine.slug"); got != "the-river" {
|
||||||
t.Fatalf("slug = %q, want the-river", got)
|
t.Fatalf("slug = %q, want the-river", got)
|
||||||
@@ -60,9 +60,9 @@ func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
|||||||
// linking order cannot matter.
|
// linking order cannot matter.
|
||||||
func TestReferenceChain(t *testing.T) {
|
func TestReferenceChain(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"a": `{"format":"{..b}"}`,
|
"a": `"{..b}"`,
|
||||||
"b": `{"format":"{..c}"}`,
|
"b": `"{..c}"`,
|
||||||
"c": `["deep"]`,
|
"c": `"deep"`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "a"); got != "deep" {
|
if got := fake(t, f, "a"); got != "deep" {
|
||||||
@@ -74,37 +74,37 @@ func TestReferenceChain(t *testing.T) {
|
|||||||
// fails at load, never at a random render.
|
// fails at load, never at a random render.
|
||||||
func TestReferenceErrors(t *testing.T) {
|
func TestReferenceErrors(t *testing.T) {
|
||||||
cases := map[string]map[string]string{
|
cases := map[string]map[string]string{
|
||||||
"missing target": {"card": `{"format":"{..nope.gone}"}`},
|
"missing target": {"card": `"{..nope.gone}"`},
|
||||||
"folder target": {"en_US/word": `["w"]`, "card": `{"format":"{..en_US}"}`},
|
"folder target": {"en_US/word": `"w"`, "card": `"{..en_US}"`},
|
||||||
"multi-variant on the path": {
|
"multi-variant on the path": {
|
||||||
"who": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"]}]`,
|
"who": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`,
|
||||||
"card": `{"format":"{..who.f}"}`,
|
"card": `"{..who.f}"`,
|
||||||
},
|
},
|
||||||
"empty reference path": {"card": `{"format":"{..}"}`},
|
"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 New must reject the cycle up front (direct, mutual, or chained).
|
// so New must reject the cycle up front (direct, mutual, or chained).
|
||||||
"direct cycle": {"a": `{"format":"x{..a}"}`},
|
"direct cycle": {"a": `"x{..a}"`},
|
||||||
"mutual cycle": {"a": `{"format":"{..b}"}`, "b": `{"format":"{..a}"}`},
|
"mutual cycle": {"a": `"{..b}"`, "b": `"{..a}"`},
|
||||||
"chain cycle": {"a": `{"format":"{..b}"}`, "b": `{"format":"{..c}"}`, "c": `{"format":"{..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 cycle through one must be caught too.
|
||||||
"calc operand cycle": {"x": `{"format":"{calc(y)}","y":[{"format":"{..x}"}]}`},
|
"calc operand cycle": {"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 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}"}}`},
|
"cycle in an unrendered field": {"cat": `{"format":"hi","x":"{..cat.x}"}`},
|
||||||
"mutual cycle between unrendered fields": {
|
"mutual cycle between unrendered fields": {
|
||||||
"cat": `{"format":"hi","x":{"format":"{..cat.y}"},"y":{"format":"{..cat.x}"}}`,
|
"cat": `{"format":"hi","x":"{..cat.y}","y":"{..cat.x}"}`,
|
||||||
},
|
},
|
||||||
"cycle in an unrendered field of a choice arm": {
|
"cycle in an unrendered field of a choice arm": {
|
||||||
"cat": `[{"format":"hi","x":{"format":"{..cat.x}"}}]`,
|
"cat": `{"format":"hi","x":"{..cat.x}"}`,
|
||||||
},
|
},
|
||||||
// The shipped layout puts categories in folders, so a cycle one level down
|
// The shipped layout puts categories in folders, so a cycle one level down
|
||||||
// is the common case, not an edge case.
|
// is the common case, not an edge case.
|
||||||
"cycle in a subfolder": {"sv_SE/a": `{"format":"x{..sv_SE.a}"}`},
|
"cycle in a subfolder": {"sv_SE/a": `"x{..sv_SE.a}"`},
|
||||||
"mutual cycle within a subfolder": {"sv_SE/a": `{"format":"{..sv_SE.b}"}`, "sv_SE/b": `{"format":"{..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": `{"format":"{..sv_SE.b}"}`, "sv_SE/b": `{"format":"{..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
|
||||||
// name a node nothing can reach and nothing would validate.
|
// name a node nothing can reach and nothing would validate.
|
||||||
"field key using the reference prefix": {"cat": `{"format":"hi","..x":{"format":"{..nope}"}}`},
|
"field key using the reference prefix": {"cat": `{"format":"hi","..x":"{..nope}"}`},
|
||||||
}
|
}
|
||||||
for name, files := range cases {
|
for name, files := range cases {
|
||||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err == nil {
|
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err == nil {
|
||||||
@@ -119,10 +119,10 @@ func TestReferenceErrors(t *testing.T) {
|
|||||||
// starts with "." — it is skipped, not rejected.
|
// starts with "." — it is skipped, not rejected.
|
||||||
func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
||||||
f := newGenerator(t, writeData(t, map[string]string{
|
f := newGenerator(t, writeData(t, map[string]string{
|
||||||
"sv_SE/ok": `["fine"]`,
|
"sv_SE/ok": `"fine"`,
|
||||||
"sv_SE/..bad": `{"format":"{..nope}"}`,
|
"sv_SE/..bad": `"{..nope}"`,
|
||||||
"sv_SE/..y/ct": `{"format":"{..nope}"}`,
|
"sv_SE/..y/ct": `"{..nope}"`,
|
||||||
".git/config": `["not data"]`,
|
".git/config": `"not data"`,
|
||||||
}), WithSeed(1))
|
}), WithSeed(1))
|
||||||
if got := f.List(); len(got) != 1 || got[0] != "sv_SE.ok" {
|
if got := f.List(); len(got) != 1 || got[0] != "sv_SE.ok" {
|
||||||
t.Fatalf("List() = %v, want only sv_SE.ok", got)
|
t.Fatalf("List() = %v, want only sv_SE.ok", got)
|
||||||
@@ -133,7 +133,7 @@ func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
|||||||
// over-rejecting: a field the format never renders may point back at its own
|
// over-rejecting: a field the format never renders may point back at its own
|
||||||
// category, which terminates, and stays renderable by path.
|
// category, which terminates, and stays renderable by path.
|
||||||
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":{"format":"see {..cat}"}}`})
|
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":"see {..cat}"}`})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "cat"); got != "hi" {
|
if got := fake(t, f, "cat"); got != "hi" {
|
||||||
t.Fatalf("cat = %q, want hi", got)
|
t.Fatalf("cat = %q, want hi", got)
|
||||||
@@ -149,9 +149,9 @@ func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
|||||||
func TestNewErrorIsDeterministic(t *testing.T) {
|
func TestNewErrorIsDeterministic(t *testing.T) {
|
||||||
cases := map[string]map[string]string{
|
cases := map[string]map[string]string{
|
||||||
"three bad references": {
|
"three bad references": {
|
||||||
"a": `{"format":"{..nope.one}"}`,
|
"a": `"{..nope.one}"`,
|
||||||
"b": `{"format":"{..nope.two}"}`,
|
"b": `"{..nope.two}"`,
|
||||||
"c": `{"format":"{..nope.three}"}`,
|
"c": `"{..nope.three}"`,
|
||||||
},
|
},
|
||||||
"two bad fields in one template": {
|
"two bad fields in one template": {
|
||||||
"cat": `{"format":"hi","aaa":{"no":1},"zzz":{"no":2}}`,
|
"cat": `{"format":"hi","aaa":{"no":1},"zzz":{"no":2}}`,
|
||||||
@@ -188,12 +188,12 @@ func TestNewErrorPathIsCanonical(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"cycle inside a choice arm",
|
"cycle inside a choice arm",
|
||||||
map[string]string{"cat": `[{"format":"hi","x":{"format":"{..cat.x}"}}]`},
|
map[string]string{"cat": `{"format":"hi","x":"{..cat.x}"}`},
|
||||||
"fejkdata: reference cycle: cat.x -> ..cat.x",
|
"fejkdata: reference cycle: cat.x -> ..cat.x",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bad reference reached through another reference",
|
"bad reference reached through another reference",
|
||||||
map[string]string{"a": `{"format":"{..b}"}`, "b": `{"format":"{..nope}"}`},
|
map[string]string{"a": `"{..b}"`, "b": `"{..nope}"`},
|
||||||
`fejkdata: b: reference {..nope}: no entry "nope"`,
|
`fejkdata: b: reference {..nope}: no entry "nope"`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func TestNewDefaultsToShippedData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWithDataPathLayersOverShipped(t *testing.T) {
|
func TestWithDataPathLayersOverShipped(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"sv_SE/word": `["only-mine"]`, "greeting": `["hej"]`})
|
dir := writeData(t, map[string]string{"sv_SE/word": `"only-mine"`, "greeting": `"hej"`})
|
||||||
f, err := New(WithDataPath(dir), WithSeed(1))
|
f, err := New(WithDataPath(dir), WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New = %v", err)
|
t.Fatalf("New = %v", err)
|
||||||
@@ -43,7 +43,7 @@ func TestWithDataPathLayersOverShipped(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUserDataMayReferenceShipped(t *testing.T) {
|
func TestUserDataMayReferenceShipped(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"greeting": `{"format":"Hej {..sv_SE.person}!"}`})
|
dir := writeData(t, map[string]string{"greeting": `"Hej {..sv_SE.person}!"`})
|
||||||
f, err := New(WithDataPath(dir), WithSeed(1))
|
f, err := New(WithDataPath(dir), WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New = %v", err)
|
t.Fatalf("New = %v", err)
|
||||||
@@ -61,7 +61,7 @@ func TestWithoutShippedDataNeedsASource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWithoutShippedDataListsOnlyOwn(t *testing.T) {
|
func TestWithoutShippedDataListsOnlyOwn(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"greeting": `["hej"]`})
|
dir := writeData(t, map[string]string{"greeting": `"hej"`})
|
||||||
f := newGenerator(t, dir, WithSeed(1))
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
if got := f.List(); !reflect.DeepEqual(got, []string{"greeting"}) {
|
if got := f.List(); !reflect.DeepEqual(got, []string{"greeting"}) {
|
||||||
t.Errorf("List() = %v, want only the loaded dir", got)
|
t.Errorf("List() = %v, want only the loaded dir", got)
|
||||||
@@ -70,10 +70,10 @@ func TestWithoutShippedDataListsOnlyOwn(t *testing.T) {
|
|||||||
|
|
||||||
func TestWithDataFS(t *testing.T) {
|
func TestWithDataFS(t *testing.T) {
|
||||||
fsys := fstest.MapFS{
|
fsys := fstest.MapFS{
|
||||||
"greeting.json": {Data: []byte(`["hej"]`)},
|
"greeting.json": {Data: []byte(`"hej"`)},
|
||||||
"nested/x.json": {Data: []byte(`{"format":"{y}","y":["z"]}`)},
|
"nested/x.json": {Data: []byte(`{"format":"{y}","y":"z"}`)},
|
||||||
".hidden.json": {Data: []byte(`["ignored"]`)},
|
".hidden.json": {Data: []byte(`"ignored"`)},
|
||||||
"broken/no.json": {Data: []byte(`["x"]`)},
|
"broken/no.json": {Data: []byte(`"x"`)},
|
||||||
}
|
}
|
||||||
f, err := New(WithoutShippedData(), WithDataFS(fsys), WithSeed(1))
|
f, err := New(WithoutShippedData(), WithDataFS(fsys), WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import "testing"
|
|||||||
// cannot quietly shift the rng stream that reproducibility depends on.
|
// cannot quietly shift the rng stream that reproducibility depends on.
|
||||||
func TestSeededOutputIsStable(t *testing.T) {
|
func TestSeededOutputIsStable(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"alt": `{"format":"{a|b}","a":["A"],"b":["B"]}`,
|
"alt": `{"format":"{a|b}","a":"A","b":"B"}`,
|
||||||
"calc": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
"calc": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
||||||
"classes": `{"format":"{digits(2)}-{int(1,9)}{int(1,9)}-{upper(2)}-{lower(2)}"}`,
|
"classes": `"{digits(2)}-{int(1,9)}{int(1,9)}-{upper(2)}-{lower(2)}"`,
|
||||||
"escapes": `{"format":"01Aa#{x}","x":["!"]}`,
|
"escapes": `{"format":"01Aa#{x}","x":"!"}`,
|
||||||
"funcs": `{"format":"{hex(6)} {int(10,99)} {float(0,1,3)} {nanoid(5)} {seq()}"}`,
|
"funcs": `"{hex(6)} {int(10,99)} {float(0,1,3)} {nanoid(5)} {seq()}"`,
|
||||||
"nested": `{"format":"{outer}","outer":[{"format":"{inner}-{digits(2)}","inner":["i"]}]}`,
|
"nested": `{"format":"{outer}","outer":{"format":"{inner}-{digits(2)}","inner":"i"}}`,
|
||||||
"ref": `{"format":"see {..alt}"}`,
|
"ref": `"see {..alt}"`,
|
||||||
"repeat": `{"format":"{w}","repeat":4,"separator":",","w":["x","y","z"]}`,
|
"repeat": `{"format":"{w}","repeat":4,"separator":",","w":["x","y","z"]}`,
|
||||||
"sums": `{"format":"9{d}{luhn()} {e}{ean()} {m}{mod11()}","d":["012345678901234"],"e":["123456789012"],"m":["12345678"]}`,
|
"sums": `{"format":"9{d}{luhn()} {e}{ean()} {m}{mod11()}","d":"012345678901234","e":"123456789012","m":"12345678"}`,
|
||||||
"weights": `[{"format":"big","weight":9},{"format":"tiny","weight":1}]`,
|
"weights": `[{"format":"big","weight":9},"tiny"]`,
|
||||||
})
|
})
|
||||||
want := map[string][]string{
|
want := map[string][]string{
|
||||||
"alt": {"A", "B", "A", "A"},
|
"alt": {"A", "B", "A", "A"},
|
||||||
|
|||||||
+58
-53
@@ -74,7 +74,7 @@ func TestClassBuiltins(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTextIsLiteral(t *testing.T) {
|
func TestTextIsLiteral(t *testing.T) {
|
||||||
if got := mustRender(t, engine(1), `{"format":"100 Main St #1 {x}","x":["A"]}`); got != "100 Main St #1 A" {
|
if got := mustRender(t, engine(1), `{"format":"100 Main St #1 {x}","x":"A"}`); got != "100 Main St #1 A" {
|
||||||
t.Fatalf("text = %q, want it verbatim", got)
|
t.Fatalf("text = %q, want it verbatim", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,21 +82,21 @@ func TestTextIsLiteral(t *testing.T) {
|
|||||||
func TestBraceEscapes(t *testing.T) {
|
func TestBraceEscapes(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for src, want := range map[string]string{
|
for src, want := range map[string]string{
|
||||||
`{"format":"{{","x":["v"]}`: "{",
|
`{"format":"{{","x":"v"}`: "{",
|
||||||
`{"format":"}}","x":["v"]}`: "}",
|
`{"format":"}}","x":"v"}`: "}",
|
||||||
`{"format":"{{x}}","x":["v"]}`: "{x}",
|
`{"format":"{{x}}","x":"v"}`: "{x}",
|
||||||
`{"format":"{{{x}}}","x":["v"]}`: "{v}",
|
`{"format":"{{{x}}}","x":"v"}`: "{v}",
|
||||||
`{"format":"a{{{{b}}}}","x":["v"]}`: "a{{b}}",
|
`{"format":"a{{{{b}}}}","x":"v"}`: "a{{b}}",
|
||||||
} {
|
} {
|
||||||
if got := mustRender(t, f, src); got != want {
|
if got := mustRender(t, f, src); got != want {
|
||||||
t.Errorf("render(%s) = %q, want %q", src, got, want)
|
t.Errorf("render(%s) = %q, want %q", src, got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for src, want := range map[string]string{
|
for src, want := range map[string]string{
|
||||||
`{"format":"x}y"}`: "}}",
|
`"x}y"`: "}}",
|
||||||
`{"format":"}"}`: "}}",
|
`"}"`: "}}",
|
||||||
`{"format":"{a{b}","a":["Q"]}`: "'{'",
|
`{"format":"{a{b}","a":"Q"}`: "'{'",
|
||||||
`{"format":"{x"}`: "unterminated",
|
`"{x"`: "unterminated",
|
||||||
} {
|
} {
|
||||||
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want)
|
t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want)
|
||||||
@@ -105,7 +105,7 @@ func TestBraceEscapes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenSubstitution(t *testing.T) {
|
func TestTokenSubstitution(t *testing.T) {
|
||||||
if got := mustRender(t, engine(1), `{"format":"{x}sson","x":["Erik"]}`); got != "Eriksson" {
|
if got := mustRender(t, engine(1), `{"format":"{x}sson","x":"Erik"}`); got != "Eriksson" {
|
||||||
t.Fatalf("token = %q, want Eriksson", got)
|
t.Fatalf("token = %q, want Eriksson", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ func TestAlternationPicksOneField(t *testing.T) {
|
|||||||
f := engine(3)
|
f := engine(3)
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
seen[mustRender(t, f, `{"format":"{a|b}","a":["A"],"b":["B"]}`)] = true
|
seen[mustRender(t, f, `{"format":"{a|b}","a":"A","b":"B"}`)] = true
|
||||||
}
|
}
|
||||||
if !seen["A"] || !seen["B"] || len(seen) != 2 {
|
if !seen["A"] || !seen["B"] || len(seen) != 2 {
|
||||||
t.Fatalf("alternation produced %v, want both A and B", seen)
|
t.Fatalf("alternation produced %v, want both A and B", seen)
|
||||||
@@ -134,7 +134,7 @@ func TestWeightSkewsDistribution(t *testing.T) {
|
|||||||
f := engine(9)
|
f := engine(9)
|
||||||
heavy := 0
|
heavy := 0
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
if mustRender(t, f, `[{"format":"H","weight":10},{"format":"L"}]`) == "H" {
|
if mustRender(t, f, `[{"format":"H","weight":10},"L"]`) == "H" {
|
||||||
heavy++
|
heavy++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,10 +173,10 @@ func TestFunctionTokenLuhn(t *testing.T) {
|
|||||||
// buffer, so a value is never re-rendered. Bodies are escaped to fix input.
|
// buffer, so a value is never re-rendered. Bodies are escaped to fix input.
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
`{"format":"811218987{luhn()}"}`: "8112189876", // personnummer body
|
`"811218987{luhn()}"`: "8112189876", // personnummer body
|
||||||
`{"format":"7992739871{luhn()}"}`: "79927398713", // classic Luhn vector
|
`"7992739871{luhn()}"`: "79927398713", // classic Luhn vector
|
||||||
`{"format":"811218-987{luhn()}"}`: "811218-9876", // '-' skipped, kept
|
`"811218-987{luhn()}"`: "811218-9876", // '-' skipped, kept
|
||||||
`{"format":"{n}{luhn()}","n":["811218987"]}`: "8112189876", // over a rendered token
|
`{"format":"{n}{luhn()}","n":"811218987"}`: "8112189876", // over a rendered token
|
||||||
}
|
}
|
||||||
for tmpl, want := range cases {
|
for tmpl, want := range cases {
|
||||||
if got := mustRender(t, f, tmpl); got != want {
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
@@ -189,7 +189,7 @@ func TestRecursionHasNoDepthLimit(t *testing.T) {
|
|||||||
// Build {format:{a}, a:[{format:{a}, a:[ ... "deep" ]]}} 50 levels deep.
|
// Build {format:{a}, a:[{format:{a}, a:[ ... "deep" ]]}} 50 levels deep.
|
||||||
tmpl := `"deep"`
|
tmpl := `"deep"`
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
tmpl = `{"format":"{a}","a":[` + tmpl + `]}`
|
tmpl = `{"format":"{a}","a":` + tmpl + `}`
|
||||||
}
|
}
|
||||||
if got := mustRender(t, engine(1), tmpl); got != "deep" {
|
if got := mustRender(t, engine(1), tmpl); got != "deep" {
|
||||||
t.Fatalf("deep recursion = %q, want deep", got)
|
t.Fatalf("deep recursion = %q, want deep", got)
|
||||||
@@ -200,51 +200,56 @@ func TestCompileErrors(t *testing.T) {
|
|||||||
// Every structural problem is caught up front, at compile/New time, never
|
// Every structural problem is caught up front, at compile/New time, never
|
||||||
// deferred to a random render that happens to hit the bad branch.
|
// deferred to a random render that happens to hit the bad branch.
|
||||||
for _, bad := range []string{
|
for _, bad := range []string{
|
||||||
`{"x":["Q"]}`, // object without "format"
|
`{"x":"Q"}`, // object without "format"
|
||||||
`{"format":"{y}","x":1}`, // a field is a bare number
|
`{"format":"{y}","x":1}`, // a field is a bare number
|
||||||
`[1, 2]`, // a choice of numbers
|
`[1, 2]`, // a choice of numbers
|
||||||
`5`, // unsupported node type
|
`5`, // unsupported node type
|
||||||
`[]`, // empty choice
|
`[]`, // empty choice
|
||||||
`{"format":"{x"}`, // unterminated brace
|
`"{x"`, // unterminated brace
|
||||||
`{"format":"x}y"}`, // a lone } must be written }}
|
`"x}y"`, // a lone } must be written }}
|
||||||
`{"format":"{a{b}","a":["Q"]}`, // a brace inside a token
|
`["x"]`, // a one-item choice is its item
|
||||||
|
`["a","a"]`, // a repeated item is a weight
|
||||||
|
`{"format":"x"}`, // an object holding only a format is a string
|
||||||
|
`[{"format":"a","weight":1},"b"]`, // weight 1 is the default
|
||||||
|
`{"format":"{x}","x":"v","repeat":1}`, // repeat 1 is the default
|
||||||
|
`{"format":"{a{b}","a":"Q"}`, // a brace inside a token
|
||||||
`"{x}"`, // a bare string has no fields to name
|
`"{x}"`, // a bare string has no fields to name
|
||||||
`{"format":"{digits(0)}"}`, // count must be positive
|
`"{digits(0)}"`, // count must be positive
|
||||||
`{"format":"{upper(x)}"}`, // count must be an integer
|
`"{upper(x)}"`, // count must be an integer
|
||||||
`{"format":"{lower()}"}`, // wrong arity
|
`"{lower()}"`, // wrong arity
|
||||||
`{"format":"{y}","x":["Q"]}`, // token names a missing field
|
`{"format":"{y}","x":"Q"}`, // token names a missing field
|
||||||
`{"format":"{}"}`, // empty token name
|
`"{}"`, // empty token name
|
||||||
`{"format":"{a|}","a":["Q"]}`, // empty alternation segment
|
`{"format":"{a|}","a":"Q"}`, // empty alternation segment
|
||||||
`[{"format":"A","weight":-1},{"format":"B"}]`, // negative weight
|
`[{"format":"A","weight":-1},"B"]`, // negative weight
|
||||||
`[{"format":"A","weight":0},{"format":"B","weight":0}]`, // weights sum to zero
|
`[{"format":"A","weight":0},{"format":"B","weight":0}]`, // weights sum to zero
|
||||||
`[{"format":"A","weight":1e308},{"format":"B","weight":1e308}]`, // weights overflow to +Inf
|
`[{"format":"A","weight":1e308},{"format":"B","weight":1e308}]`, // weights overflow to +Inf
|
||||||
`[{"format":"A","weight":"heavy"}]`, // non-numeric weight
|
`{"format":"A","weight":"heavy"}`, // non-numeric weight
|
||||||
`{"format":"x","repeat":0}`, // repeat below 1
|
`{"format":"x","repeat":0}`, // repeat below 1
|
||||||
`{"format":"x","repeat":-2}`, // negative repeat
|
`{"format":"x","repeat":-2}`, // negative repeat
|
||||||
`{"format":"x","repeat":1.5}`, // non-integer repeat
|
`{"format":"x","repeat":1.5}`, // non-integer repeat
|
||||||
`{"format":"x","repeat":"two"}`, // non-numeric repeat
|
`{"format":"x","repeat":"two"}`, // non-numeric repeat
|
||||||
`{"format":"x","repeat":2,"separator":5}`, // non-string separator
|
`{"format":"x","repeat":2,"separator":5}`, // non-string separator
|
||||||
`{"format":"{nope()}"}`, // unknown function
|
`"{nope()}"`, // unknown function
|
||||||
`{"format":"{luhn(x)}"}`, // function given args it takes none of
|
`"{luhn(x)}"`, // function given args it takes none of
|
||||||
`{"format":"{luhn(}"}`, // malformed function token
|
`"{luhn(}"`, // malformed function token
|
||||||
`{"format":"{int(1)}"}`, // wrong arity
|
`"{int(1)}"`, // wrong arity
|
||||||
`{"format":"{int(a,b)}"}`, // non-integer args
|
`"{int(a,b)}"`, // non-integer args
|
||||||
`{"format":"{int(5,1)}"}`, // min > max
|
`"{int(5,1)}"`, // min > max
|
||||||
`{"format":"{hex(0)}"}`, // count must be positive
|
`"{hex(0)}"`, // count must be positive
|
||||||
`{"format":"{nanoid(-1)}"}`, // negative count
|
`"{nanoid(-1)}"`, // negative count
|
||||||
`{"format":"{base64(0)}"}`, // count must be positive
|
`"{base64(0)}"`, // count must be positive
|
||||||
`{"format":"{float(1,2)}"}`, // wrong arity
|
`"{float(1,2)}"`, // wrong arity
|
||||||
`{"format":"{float(1,2,-1)}"}`, // negative decimals
|
`"{float(1,2,-1)}"`, // negative decimals
|
||||||
`{"format":"{iban(US)}"}`, // unsupported country
|
`"{iban(US)}"`, // unsupported country
|
||||||
`{"format":"{seq(a,b)}"}`, // seq takes at most one name
|
`"{seq(a,b)}"`, // seq takes at most one name
|
||||||
`{"format":"{calc()}"}`, // calc needs an expression
|
`"{calc()}"`, // calc needs an expression
|
||||||
`{"format":"{calc(1 +)}"}`, // dangling operator
|
`"{calc(1 +)}"`, // dangling operator
|
||||||
`{"format":"{calc((1 + 2)}"}`, // unbalanced parenthesis
|
`"{calc((1 + 2)}"`, // unbalanced parenthesis
|
||||||
`{"format":"{calc(1 2)}"}`, // two operands, no operator
|
`"{calc(1 2)}"`, // two operands, no operator
|
||||||
`{"format":"{calc(price)}"}`, // operand names no field
|
`"{calc(price)}"`, // operand names no field
|
||||||
`{"format":"{calc(1, 2, 3)}"}`, // too many args
|
`"{calc(1, 2, 3)}"`, // too many args
|
||||||
`{"format":"{calc(1, x)}"}`, // decimals arg not an integer
|
`"{calc(1, x)}"`, // decimals arg not an integer
|
||||||
`{"format":"{calc(1, -1)}"}`, // decimals negative
|
`"{calc(1, -1)}"`, // decimals negative
|
||||||
} {
|
} {
|
||||||
if _, err := compile(parse(t, bad)); err == nil {
|
if _, err := compile(parse(t, bad)); err == nil {
|
||||||
t.Errorf("compile(%s) = nil error, want error", bad)
|
t.Errorf("compile(%s) = nil error, want error", bad)
|
||||||
@@ -255,7 +260,7 @@ func TestCompileErrors(t *testing.T) {
|
|||||||
func TestFakePathNavigation(t *testing.T) {
|
func TestFakePathNavigation(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
f.categories = map[string]node{
|
f.categories = map[string]node{
|
||||||
"addr": compiled(t, `[{"format":"{street}","street":["Main"]}]`),
|
"addr": compiled(t, `{"format":"{street}","street":"Main"}`),
|
||||||
}
|
}
|
||||||
if got, err := f.Fake("addr"); err != nil || !strings.Contains(got, "Main") {
|
if got, err := f.Fake("addr"); err != nil || !strings.Contains(got, "Main") {
|
||||||
t.Fatalf("Fake(addr) = %q, %v", got, err)
|
t.Fatalf("Fake(addr) = %q, %v", got, err)
|
||||||
@@ -288,7 +293,7 @@ func TestGrowIsALowerBound(t *testing.T) {
|
|||||||
"9{d}{luhn()}",
|
"9{d}{luhn()}",
|
||||||
"{a|b} and {a|b}",
|
"{a|b} and {a|b}",
|
||||||
} {
|
} {
|
||||||
src := `{"format":` + quote(format) + `,"x":["1"],"a":["A"],"b":["B"],"d":["012345678901234"]}`
|
src := `{"format":` + quote(format) + `,"x":"1","a":"A","b":"B","d":"012345678901234"}`
|
||||||
tmpl, ok := compiled(t, src).(*template)
|
tmpl, ok := compiled(t, src).(*template)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("format %q did not compile to a template", format)
|
t.Fatalf("format %q did not compile to a template", format)
|
||||||
|
|||||||
Reference in New Issue
Block a user