Add control logic engine, panel logic dialogs, logic-edit restriction
Introduce server-side control-logic flow graphs (cron/alarm triggers, Lua blocks) with CRUD endpoints, panel-logic lifecycle triggers and user-interaction dialog nodes, and a synthetic node-graph editor. Add an optional logic-editor allowlist (server.logic_editors) gating who may add/edit panel logic and control logic, surfaced via /api/v1/me and enforced in the API; hide logic affordances in the UI accordingly. Update README, example config, and functional/technical specs to cover all current features (plot panels, panel/control logic, local variables, access control) and refresh the in-app manual and contextual help. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package controllogic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEvalExpr(t *testing.T) {
|
||||
resolve := func(ds, name string) float64 {
|
||||
switch {
|
||||
case ds == "stub" && name == "x":
|
||||
return 10
|
||||
case ds == "local" && name == "y":
|
||||
return 3
|
||||
case ds == "sys" && name == "dt":
|
||||
return 0.5
|
||||
}
|
||||
return math.NaN()
|
||||
}
|
||||
cases := []struct {
|
||||
expr string
|
||||
want float64
|
||||
}{
|
||||
{"1 + 2 * 3", 7},
|
||||
{"(1 + 2) * 3", 9},
|
||||
{"10 % 3", 1},
|
||||
{"{stub:x} + y", 13},
|
||||
{"{stub:x} > 5 ? 1 : 0", 1},
|
||||
{"min(4, 2, 8)", 2},
|
||||
{"max(4, 2, 8)", 8},
|
||||
{"abs(-5)", 5},
|
||||
{"sqrt(16)", 4},
|
||||
{"2 < 3 && 3 < 4", 1},
|
||||
{"!0", 1},
|
||||
{"-{stub:x}", -10},
|
||||
{"{sys:dt} * 2", 1},
|
||||
{"true + false", 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := EvalExpr(c.expr, resolve)
|
||||
if math.Abs(got-c.want) > 1e-9 {
|
||||
t.Errorf("EvalExpr(%q) = %v, want %v", c.expr, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalExprErrors(t *testing.T) {
|
||||
r := func(ds, name string) float64 { return 0 }
|
||||
for _, bad := range []string{"1 +", "(1", "1 2", "{unterminated"} {
|
||||
if v := EvalExpr(bad, r); !math.IsNaN(v) {
|
||||
t.Errorf("EvalExpr(%q) = %v, want NaN", bad, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectRefs(t *testing.T) {
|
||||
refs := CollectRefs("{stub:x} + y + {epics:MY:PV} * z")
|
||||
got := map[string]bool{}
|
||||
for _, r := range refs {
|
||||
got[r.DS+":"+r.Name] = true
|
||||
}
|
||||
for _, want := range []string{"stub:x", "local:y", "epics:MY:PV", "local:z"} {
|
||||
if !got[want] {
|
||||
t.Errorf("CollectRefs missing %q (got %v)", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckExpr(t *testing.T) {
|
||||
if msg := CheckExpr("1 + 2"); msg != "" {
|
||||
t.Errorf("CheckExpr valid returned %q", msg)
|
||||
}
|
||||
if msg := CheckExpr("1 +"); msg == "" {
|
||||
t.Errorf("CheckExpr invalid returned empty")
|
||||
}
|
||||
if msg := CheckExpr(""); msg != "" {
|
||||
t.Errorf("CheckExpr empty returned %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEpicsRefSplitFirstColon(t *testing.T) {
|
||||
refs := CollectRefs("{epics:SR:BPM:01:X}")
|
||||
if len(refs) != 1 || refs[0].DS != "epics" || refs[0].Name != "SR:BPM:01:X" {
|
||||
t.Errorf("got %+v, want epics / SR:BPM:01:X", refs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user