Improving all side of app

This commit is contained in:
Martino Ferrari
2026-06-20 14:28:28 +02:00
parent 901b87d407
commit 446de7f1ee
33 changed files with 1758 additions and 389 deletions
+130
View File
@@ -0,0 +1,130 @@
package synthetic
import (
"math"
"testing"
)
// evalDef compiles a SignalDef and evaluates it against per-source values keyed
// by source node id.
func evalDef(t *testing.T, def SignalDef, srcVals map[string]float64) float64 {
t.Helper()
rg, err := compileGraph(def)
if err != nil {
t.Fatalf("compileGraph: %v", err)
}
out, err := rg.eval(srcVals)
if err != nil {
t.Fatalf("eval: %v", err)
}
return out
}
// TestGraphMultiInputDAG verifies that an intermediate op can take two
// independently-wired sources — the capability the old linear pipeline lacked.
func TestGraphMultiInputDAG(t *testing.T) {
def := SignalDef{
Name: "diff",
Graph: &Graph{
Output: "out",
Nodes: []GraphNode{
{ID: "a", Kind: "source", DS: "x", Signal: "left"},
{ID: "b", Kind: "source", DS: "x", Signal: "right"},
{ID: "sub", Kind: "op", Op: "subtract", Inputs: []string{"a", "b"}},
{ID: "out", Kind: "output", Inputs: []string{"sub"}},
},
},
}
got := evalDef(t, def, map[string]float64{"a": 10, "b": 3})
if got != 7 {
t.Errorf("subtract DAG: want 7, got %v", got)
}
}
// TestGraphExprNamedInputs verifies expr nodes bind arbitrary named inputs in
// wired order.
func TestGraphExprNamedInputs(t *testing.T) {
def := SignalDef{
Name: "formula",
Graph: &Graph{
Output: "out",
Nodes: []GraphNode{
{ID: "a", Kind: "source", DS: "x", Signal: "p"},
{ID: "b", Kind: "source", DS: "x", Signal: "q"},
{ID: "e", Kind: "op", Op: "expr", Inputs: []string{"a", "b"},
Params: map[string]any{"expr": "price * qty", "vars": []any{"price", "qty"}}},
{ID: "out", Kind: "output", Inputs: []string{"e"}},
},
},
}
got := evalDef(t, def, map[string]float64{"a": 4, "b": 2.5})
if math.Abs(got-10) > 1e-9 {
t.Errorf("expr named inputs: want 10, got %v", got)
}
}
// TestGraphFanInToExpr exercises a non-trivial DAG: two ops feeding one expr.
func TestGraphFanInToExpr(t *testing.T) {
def := SignalDef{
Name: "combo",
Graph: &Graph{
Output: "out",
Nodes: []GraphNode{
{ID: "a", Kind: "source", DS: "x", Signal: "p"},
{ID: "b", Kind: "source", DS: "x", Signal: "q"},
{ID: "g", Kind: "op", Op: "gain", Inputs: []string{"a"}, Params: map[string]any{"gain": 2.0}},
{ID: "o", Kind: "op", Op: "offset", Inputs: []string{"b"}, Params: map[string]any{"offset": 1.0}},
{ID: "e", Kind: "op", Op: "expr", Inputs: []string{"g", "o"}, Params: map[string]any{"expr": "a + b"}},
{ID: "out", Kind: "output", Inputs: []string{"e"}},
},
},
}
// g = 5*2 = 10 ; o = 4+1 = 5 ; a+b = 15
got := evalDef(t, def, map[string]float64{"a": 5, "b": 4})
if math.Abs(got-15) > 1e-9 {
t.Errorf("fan-in DAG: want 15, got %v", got)
}
}
// TestGraphLegacyConversion verifies the linear Inputs+Pipeline form still
// evaluates correctly via the graph runtime.
func TestGraphLegacyConversion(t *testing.T) {
def := SignalDef{
Name: "legacy",
DS: "x",
Signal: "p",
Pipeline: []NodeDef{{Type: "gain", Params: map[string]any{"gain": 3.0}}},
}
rg, err := compileGraph(def)
if err != nil {
t.Fatalf("compileGraph: %v", err)
}
if len(rg.sources) != 1 {
t.Fatalf("want 1 source, got %d", len(rg.sources))
}
got, err := rg.eval(map[string]float64{rg.sources[0].id: 4})
if err != nil {
t.Fatalf("eval: %v", err)
}
if got != 12 {
t.Errorf("legacy gain: want 12, got %v", got)
}
}
// TestGraphCycleRejected ensures a cyclic graph is refused at compile time.
func TestGraphCycleRejected(t *testing.T) {
def := SignalDef{
Name: "cyclic",
Graph: &Graph{
Output: "out",
Nodes: []GraphNode{
{ID: "a", Kind: "op", Op: "gain", Inputs: []string{"b"}, Params: map[string]any{"gain": 1.0}},
{ID: "b", Kind: "op", Op: "gain", Inputs: []string{"a"}, Params: map[string]any{"gain": 1.0}},
{ID: "out", Kind: "output", Inputs: []string{"a"}},
},
},
}
if _, err := compileGraph(def); err == nil {
t.Error("expected cycle to be rejected")
}
}