oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package calc
2
3import (
4 "testing"
5 "time"
6)
7
8// zzTestFn is a minimal passthrough registered purely so the two
9// call-argument cycle tests below can exercise evalCall's argument
10// evaluation. Task 7 ships no real functions — funcTable starts empty,
11// task 8 populates it — so nothing else in this package's own tests
12// would otherwise evaluate a Call's arguments at all. The name is
13// deliberately not a real spreadsheet function's name, to avoid
14// colliding with whatever task 8 registers later.
15func init() {
16 funcTable["ZZTESTFN"] = func(args []Value) Value {
17 if len(args) == 0 {
18 return Value{}
19 }
20 return args[0]
21 }
22}
23
24// evalWithTimeout runs fn in a goroutine and fails the test if it does
25// not return within 2 seconds — the same pattern TestCycleDetection uses.
26// The failure mode of broken cycle detection is a wedged process, so a
27// broken test must fail, not hang alongside it.
28func evalWithTimeout(t *testing.T, fn func() Value) Value {
29 t.Helper()
30 done := make(chan Value, 1)
31 go func() { done <- fn() }()
32 select {
33 case v := <-done:
34 return v
35 case <-time.After(2 * time.Second):
36 t.Fatal("cycle detection hung")
37 return Value{}
38 }
39}
40
41func assertCircular(t *testing.T, v Value) {
42 t.Helper()
43 if v.Kind != KindErr || v.Err != ErrCircular {
44 t.Fatalf("cycle = %v, want #CIRCULAR!", v)
45 }
46}
47
48// A cell that references itself is a one-node cycle: the simplest case,
49// and the one most likely to be broken by an off-by-one in the colour
50// check (e.g. checking "grey" only for a *different* node).
51func TestCycleDetectionSelfReference(t *testing.T) {
52 g := NewGraph()
53 g.SetFormula(cellA1, mustParse(t, "A1+1"))
54 assertCircular(t, evalWithTimeout(t, func() Value { return g.Value(cellA1, env) }))
55}
56
57// A longer ring (A1 -> B1 -> C1 -> A1) exercises more than one grey node
58// on the stack at once before the cycle closes.
59func TestCycleDetectionLongRing(t *testing.T) {
60 g := NewGraph()
61 g.SetFormula(cellA1, mustParse(t, "B1"))
62 g.SetFormula(cellB1, mustParse(t, "C1"))
63 g.SetFormula(cellC1, mustParse(t, "A1"))
64 assertCircular(t, evalWithTimeout(t, func() Value { return g.Value(cellA1, env) }))
65}
66
67// A cycle reachable through a Range argument to a call: evalCallArg's
68// range branch calls env.CellValue directly for each cell in the range
69// rather than re-entering Eval, so this specifically checks that the
70// direct call still goes through the graph's colour-marked CellValue
71// (i.e. it is not a way to bypass cycle detection).
72func TestCycleDetectionThroughRangeArgument(t *testing.T) {
73 g := NewGraph()
74 g.SetFormula(cellA1, mustParse(t, "ZZTESTFN(B1:B1)"))
75 g.SetFormula(cellB1, mustParse(t, "A1"))
76 assertCircular(t, evalWithTimeout(t, func() Value { return g.Value(cellA1, env) }))
77}
78
79// A cycle reachable through an ordinary (non-range) call argument: the
80// general case of evalCall's argument evaluation participating correctly
81// in cycle detection.
82func TestCycleDetectionThroughCallArgument(t *testing.T) {
83 g := NewGraph()
84 g.SetFormula(cellA1, mustParse(t, "B1"))
85 g.SetFormula(cellB1, mustParse(t, "ZZTESTFN(A1)"))
86 assertCircular(t, evalWithTimeout(t, func() Value { return g.Value(cellA1, env) }))
87}
88