Tests for what a calc operand's hold actually pins
A calc renders its operand whole, so the draw it holds is that one node's
value. The render closure was too wide: it pulled in shared descendants, so
two operands drawing from one source were rejected — two dice over one
{..die}, or {w} x {h} over one {..dim} — though each is drawn once, shown
once, and cannot disagree. main accepts all of them.
So the fence is another route naming the operand's own node, and these pin
both sides: a reference to the operand, one level down, or wrapped in a
choice is rejected; two names drawing from one source, a sibling the operand
never renders, and a literal operand are accepted. That reverses the case
added earlier this round, which read a shared source as a second spelling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+41
-23
@@ -142,46 +142,64 @@ func TestALevelRenderedOnlyByAPathTokenIsHeld(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestACalcOperandIsHeldAgainstEveryRoute pins that a {calc()} operand is fenced
|
// TestACalcOperandIsHeldAgainstEveryRoute pins what a {calc()} operand's hold
|
||||||
// the way a path-read level is. The operand is drawn once and held; a {..path}
|
// fences. A calc renders its operand whole, so the draw it holds is that one node's
|
||||||
// spelling of the same field renders it afresh, so the value shown would not be
|
// value: another route conflicts only by naming that same node, and then the value
|
||||||
// the value computed — the disagreement the hold exists to remove.
|
// shown is not the value computed. Two names that merely draw from one source are
|
||||||
|
// two draws, as {word} {word} is — the rule everywhere else in the engine.
|
||||||
func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
||||||
rejected := map[string]string{
|
rejected := map[string]map[string]string{
|
||||||
"a reference beside the operand": `{"format":"{..cat.net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
"a reference beside the operand": {
|
||||||
"a reference one level down": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
"cat": `{"format":"{..cat.net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
||||||
`"q":{"format":"{..cat.net}"}}`,
|
},
|
||||||
|
"a reference one level down": {
|
||||||
|
"cat": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
||||||
|
`"q":{"format":"{..cat.net}"}}`,
|
||||||
|
},
|
||||||
|
// A reference ending at the operand binds the choice itself, not a variant,
|
||||||
|
// so wrapping the operand in one changes nothing.
|
||||||
|
"a reference to an operand wrapped in a choice": {
|
||||||
|
"cat": `{"format":"{calc(n * 2, 2)} {q}","n":[{"format":"{v}","v":["1","2"]}],` +
|
||||||
|
`"q":{"format":"{..cat.n}"}}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, file := range rejected {
|
for name, files := range rejected {
|
||||||
_, err := New([]string{writeData(t, map[string]string{"cat": file})})
|
_, err := New([]string{writeData(t, files)})
|
||||||
if err == nil || !strings.Contains(err.Error(), "a {calc()} also reads") {
|
if err == nil || !strings.Contains(err.Error(), "a {calc()} also reads") {
|
||||||
t.Errorf("%s: New = %v, want the second route to the operand rejected", name, err)
|
t.Errorf("%s: New = %v, want the second route to the operand rejected", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A reference reaching what the operand renders *through* is a second route
|
|
||||||
// too, however the operand comes by its value.
|
|
||||||
_, err := New([]string{writeData(t, map[string]string{
|
|
||||||
"common": `["1","2"]`,
|
|
||||||
"cat": `{"format":"{calc(net * 2, 2)} {..common}","net":{"format":"{..common}"}}`,
|
|
||||||
})})
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "a {calc()} also reads") {
|
|
||||||
t.Errorf("New = %v, want the reference to what the operand renders rejected", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
accepted := map[string]map[string]string{
|
accepted := map[string]map[string]string{
|
||||||
// The spellings that read the one draw: the operand itself, and a bare token.
|
// The spellings that read the one draw: the operand itself, and a bare token.
|
||||||
"only the operand and a bare token": {
|
"only the operand and a bare token": {
|
||||||
"cat": `{"format":"{net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
"cat": `{"format":"{net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
||||||
},
|
},
|
||||||
// An operand's draw fixes the value it renders and nothing else, so a sibling
|
// A sibling the operand's format never renders is no part of its value. A
|
||||||
// its format never renders cannot disagree with it. A path head differs: a
|
// path head differs — a path may read into anything the level contains —
|
||||||
// path may read into anything the level contains, which is why that half of
|
// which is why that half of the fence covers containment.
|
||||||
// the fence still covers containment.
|
|
||||||
"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":{"format":"{..cat.net.spare}"}}`,
|
||||||
},
|
},
|
||||||
|
// 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.
|
||||||
|
"two operands sharing one source": {
|
||||||
|
"die": `["1","2","3","4","5","6"]`,
|
||||||
|
"cat": `{"format":"{d1} + {d2} = {calc(d1 + d2, 0)}","d1":{"format":"{..die}"},` +
|
||||||
|
`"d2":{"format":"{..die}"}}`,
|
||||||
|
},
|
||||||
|
// Likewise a reference drawing from what the operand draws from: {..common}
|
||||||
|
// and net are two names, not two spellings of one field.
|
||||||
|
"a reference to what an operand renders through": {
|
||||||
|
"common": `["1","2"]`,
|
||||||
|
"cat": `{"format":"{calc(net * 2, 2)} {..common}","net":{"format":"{..common}"}}`,
|
||||||
|
},
|
||||||
|
// A fixed string cannot disagree with itself, so it needs no fence.
|
||||||
|
"a literal operand named twice": {
|
||||||
|
"cat": `{"format":"{calc(n * 2, 0)} {..cat.n}","n":"5"}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, files := range accepted {
|
for name, files := range accepted {
|
||||||
if _, err := New([]string{writeData(t, files)}); err != nil {
|
if _, err := New([]string{writeData(t, files)}); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user