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:
+40
-22
@@ -142,46 +142,64 @@ func TestALevelRenderedOnlyByAPathTokenIsHeld(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestACalcOperandIsHeldAgainstEveryRoute pins that a {calc()} operand is fenced
|
||||
// the way a path-read level is. The operand is drawn once and held; a {..path}
|
||||
// spelling of the same field renders it afresh, so the value shown would not be
|
||||
// the value computed — the disagreement the hold exists to remove.
|
||||
// TestACalcOperandIsHeldAgainstEveryRoute pins what a {calc()} operand's hold
|
||||
// fences. A calc renders its operand whole, so the draw it holds is that one node's
|
||||
// value: another route conflicts only by naming that same node, and then the value
|
||||
// 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) {
|
||||
rejected := map[string]string{
|
||||
"a reference beside the operand": `{"format":"{..cat.net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
||||
"a reference one level down": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
||||
rejected := map[string]map[string]string{
|
||||
"a reference beside the operand": {
|
||||
"cat": `{"format":"{..cat.net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
||||
},
|
||||
"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 {
|
||||
_, err := New([]string{writeData(t, map[string]string{"cat": file})})
|
||||
for name, files := range rejected {
|
||||
_, err := New([]string{writeData(t, files)})
|
||||
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)
|
||||
}
|
||||
}
|
||||
// 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{
|
||||
// The spellings that read the one draw: the operand itself, 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"]}`,
|
||||
},
|
||||
// An operand's draw fixes the value it renders and nothing else, so a sibling
|
||||
// its format never renders cannot disagree with it. A path head differs: a
|
||||
// path may read into anything the level contains, which is why that half of
|
||||
// the fence still covers containment.
|
||||
// A sibling the operand's format never renders is no part of its value. A
|
||||
// path head differs — a path may read into anything the level contains —
|
||||
// which is why that half of the fence covers containment.
|
||||
"a reference to a sibling the operand never renders": {
|
||||
"cat": `{"format":"{calc(net * 2, 2)} {unit}",` +
|
||||
`"net":{"format":"{v}","v":["1","2"],"spare":["kg","lb"]},` +
|
||||
`"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 {
|
||||
if _, err := New([]string{writeData(t, files)}); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user