package controllogic import ( "context" "reflect" "testing" ) // runFlowOnce compiles g, wires a minimal engine/context onto the compiled graph // (enough for the run/follow path and the lock-free emitDebug guard), then drives // the flow from triggerID synchronously and returns the compiled graph so callers // can inspect locals. func runFlowOnce(t *testing.T, g Graph, triggerID string) *compiledGraph { t.Helper() cg := compile(g) cg.engine = &Engine{} cg.genCtx = context.Background() R := func(ds, name string) Value { switch ds { case "local": return cg.getLocal(name) default: return 0.0 } } ctx := &runCtx{fired: triggerID, resolve: R} cg.follow(triggerID, "out", ctx) return cg } func TestArrayPushNode(t *testing.T) { g := Graph{ ID: "g", Name: "n", StateVars: []StateVar{{Name: "buf", Type: "array", Initial: "[1]", Sizing: "dynamic"}}, Nodes: []Node{ {ID: "t", Kind: "trigger.timer", Params: map[string]string{"interval": "1000"}}, {ID: "p", Kind: "action.array.push", Params: map[string]string{"array": "buf", "expr": "5"}}, }, Wires: []Wire{{From: "t", To: "p"}}, } cg := runFlowOnce(t, g, "t") if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{1.0, 5.0}) { t.Fatalf("after push buf = %#v", got) } } func TestArrayClearNode(t *testing.T) { g := Graph{ ID: "g", Name: "n", StateVars: []StateVar{{Name: "buf", Type: "array", Initial: "[1,2,3]", Sizing: "fixed", Capacity: 3}}, Nodes: []Node{ {ID: "t", Kind: "trigger.timer", Params: map[string]string{"interval": "1000"}}, {ID: "c", Kind: "action.array.clear", Params: map[string]string{"array": "buf"}}, }, Wires: []Wire{{From: "t", To: "c"}}, } cg := runFlowOnce(t, g, "t") if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{0.0, 0.0, 0.0}) { t.Fatalf("after clear buf = %#v", got) } } func TestArraySetNode(t *testing.T) { g := Graph{ ID: "g", Name: "n", StateVars: []StateVar{{Name: "buf", Type: "array", Initial: "[0,0,0]", Sizing: "dynamic"}}, Nodes: []Node{ {ID: "t", Kind: "trigger.timer", Params: map[string]string{"interval": "1000"}}, {ID: "s", Kind: "action.array.set", Params: map[string]string{"array": "buf", "index": "1", "expr": "9"}}, }, Wires: []Wire{{From: "t", To: "s"}}, } cg := runFlowOnce(t, g, "t") if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{0.0, 9.0, 0.0}) { t.Fatalf("after set buf = %#v", got) } } func TestArrayRemoveNode(t *testing.T) { g := Graph{ ID: "g", Name: "n", StateVars: []StateVar{{Name: "buf", Type: "array", Initial: "[10,20,30]", Sizing: "dynamic"}}, Nodes: []Node{ {ID: "t", Kind: "trigger.timer", Params: map[string]string{"interval": "1000"}}, {ID: "r", Kind: "action.array.remove", Params: map[string]string{"array": "buf", "index": "-1"}}, }, Wires: []Wire{{From: "t", To: "r"}}, } cg := runFlowOnce(t, g, "t") if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{10.0, 20.0}) { t.Fatalf("after remove buf = %#v", got) } } func TestArrayPopNode(t *testing.T) { g := Graph{ ID: "g", Name: "n", StateVars: []StateVar{{Name: "buf", Type: "array", Initial: "[1,2,3]", Sizing: "dynamic"}}, Nodes: []Node{ {ID: "t", Kind: "trigger.timer", Params: map[string]string{"interval": "1000"}}, {ID: "p", Kind: "action.array.pop", Params: map[string]string{"array": "buf"}}, }, Wires: []Wire{{From: "t", To: "p"}}, } cg := runFlowOnce(t, g, "t") if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{1.0, 2.0}) { t.Fatalf("after pop buf = %#v", got) } } func TestCompileInitsLocalsFromDecls(t *testing.T) { g := Graph{ ID: "g", Name: "n", StateVars: []StateVar{ {Name: "count", Type: "number", Initial: "7"}, {Name: "flag", Type: "bool", Initial: "true"}, {Name: "buf", Type: "array", Initial: "[1,2,3]", Sizing: "capped", Capacity: 4}, }, } cg := compile(g) if got := cg.getLocal("count"); got != Value(7.0) { t.Fatalf("count = %#v", got) } if got := cg.getLocal("flag"); got != Value(1.0) { t.Fatalf("flag = %#v", got) } if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{1.0, 2.0, 3.0}) { t.Fatalf("buf = %#v", got) } } func TestSetLocalAppliesSizing(t *testing.T) { g := Graph{ID: "g", Name: "n", StateVars: []StateVar{ {Name: "buf", Type: "array", Initial: "[]", Sizing: "capped", Capacity: 2}, }} cg := compile(g) cg.setLocal("buf", []Value{1.0, 2.0, 3.0, 4.0}) if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{3.0, 4.0}) { t.Fatalf("sized buf = %#v", got) } } func TestResolverReturnsLocalValue(t *testing.T) { g := Graph{ID: "g", Name: "n", StateVars: []StateVar{ {Name: "buf", Type: "array", Initial: "[10,20]", Sizing: "dynamic"}, }} cg := compile(g) R := func(ds, name string) Value { if ds == "local" { return cg.getLocal(name) } return 0.0 } if v := EvalExpr("buf[1]", R); v != 20 { t.Fatalf("buf[1] = %v", v) } if v := EvalExpr("sum(buf)", R); v != 30 { t.Fatalf("sum(buf) = %v", v) } } func TestEmitDebugAcceptsArray(t *testing.T) { g := Graph{ID: "g", Name: "n"} cg := compile(g) cg.engine = &Engine{} // no observer installed; emitDebug must not panic cg.emitDebug("x", []Value{1.0, 2.0}, true) cg.emitDebug("x", 3.0, true) }