package confmgr import ( "testing" ) func fptr(f float64) *float64 { return &f } func sampleSet() ConfigSet { return ConfigSet{ Name: "PSU", Description: "power supply config", Parameters: []Parameter{ {Key: "voltage", DS: "epics", Signal: "PSU:V", Type: TypeFloat, Default: 12.0, Mandatory: true, Min: fptr(0), Max: fptr(48)}, {Key: "enabled", DS: "epics", Signal: "PSU:EN", Type: TypeBool, Default: false}, }, } } func TestCreateAndGetSet(t *testing.T) { s, err := New(t.TempDir()) if err != nil { t.Fatal(err) } out, err := s.CreateSet(sampleSet(), "initial") if err != nil { t.Fatal(err) } if out.ID == "" { t.Fatal("expected generated ID") } if out.Version != 1 { t.Errorf("version: want 1, got %d", out.Version) } got, err := s.GetSet(out.ID) if err != nil { t.Fatal(err) } if got.Name != "PSU" || len(got.Parameters) != 2 { t.Errorf("round-trip mismatch: %+v", got) } } func TestRuleBlocksInvalidInstance(t *testing.T) { s, _ := New(t.TempDir()) set, _ := s.CreateSet(sampleSet(), "") if _, err := s.CreateRule(ConfigRule{ Name: "voltage cap", SetID: set.ID, Source: "voltage: <=24", }, ""); err != nil { t.Fatal(err) } // In-range value passes. if _, err := s.CreateInstance(ConfigInstance{ Name: "ok", SetID: set.ID, Values: map[string]any{"voltage": 20.0}, }, ""); err != nil { t.Fatalf("expected in-range instance to save, got %v", err) } // Out-of-range value is rejected by the rule. _, err := s.CreateInstance(ConfigInstance{ Name: "bad", SetID: set.ID, Values: map[string]any{"voltage": 40.0}, }, "") if err == nil { t.Fatalf("expected rule violation to block save") } var re *RuleError if !asRuleError(err, &re) { t.Fatalf("expected *RuleError, got %T: %v", err, err) } } func TestRuleTransformPersists(t *testing.T) { s, _ := New(t.TempDir()) set := sampleSet() max := 48.0 set.Parameters = append(set.Parameters, Parameter{Key: "vmax", DS: "epics", Signal: "PSU:VMAX", Type: TypeFloat, Min: &max}) created, _ := s.CreateSet(set, "") // Rule derives vmax = voltage * 2 (within range for voltage=20 -> 40). if _, err := s.CreateRule(ConfigRule{ Name: "vmax derive", SetID: created.ID, Source: "voltage: number\nvmax: voltage * 2", }, ""); err != nil { t.Fatal(err) } inst, err := s.CreateInstance(ConfigInstance{ Name: "x", SetID: created.ID, Values: map[string]any{"voltage": 20.0}, }, "") if err != nil { t.Fatal(err) } if f, _ := toFloat(inst.Values["vmax"]); f != 40 { t.Fatalf("expected derived vmax=40 to persist, got %v", inst.Values["vmax"]) } } func TestDisabledRuleSkipped(t *testing.T) { s, _ := New(t.TempDir()) set, _ := s.CreateSet(sampleSet(), "") disabled := false if _, err := s.CreateRule(ConfigRule{ Name: "voltage cap", SetID: set.ID, Enabled: &disabled, Source: "voltage: <=24", }, ""); err != nil { t.Fatal(err) } // A value that would violate the rule still saves because the rule is off. if _, err := s.CreateInstance(ConfigInstance{ Name: "bad", SetID: set.ID, Values: map[string]any{"voltage": 40.0}, }, ""); err != nil { t.Fatalf("expected disabled rule to be skipped, got %v", err) } } func asRuleError(err error, target **RuleError) bool { for err != nil { if re, ok := err.(*RuleError); ok { *target = re return true } type unwrapper interface{ Unwrap() error } u, ok := err.(unwrapper) if !ok { return false } err = u.Unwrap() } return false } func TestSetVersioning(t *testing.T) { s, _ := New(t.TempDir()) created, _ := s.CreateSet(sampleSet(), "") id := created.ID // Update -> v2. upd := sampleSet() upd.Description = "edited" v2, err := s.UpdateSet(id, upd, "edit") if err != nil { t.Fatal(err) } if v2.Version != 2 { t.Fatalf("version after update: want 2, got %d", v2.Version) } versions, err := s.Versions(KindSet, id) if err != nil { t.Fatal(err) } if len(versions) != 2 { t.Fatalf("want 2 versions, got %d", len(versions)) } if !versions[0].Current || versions[0].Version != 2 { t.Errorf("newest should be current v2: %+v", versions[0]) } // Old version still retrievable. old, err := s.GetSetVersion(id, 1) if err != nil { t.Fatal(err) } if old.Description != "power supply config" { t.Errorf("v1 description changed: %q", old.Description) } // Promote v1 -> becomes v3 with original content. if err := s.Promote(KindSet, id, 1); err != nil { t.Fatal(err) } cur, _ := s.GetSet(id) if cur.Version != 3 || cur.Description != "power supply config" { t.Errorf("after promote: want v3 original desc, got v%d %q", cur.Version, cur.Description) } } func TestForkSet(t *testing.T) { s, _ := New(t.TempDir()) created, _ := s.CreateSet(sampleSet(), "") _, _ = s.UpdateSet(created.ID, sampleSet(), "") newID, err := s.Fork(KindSet, created.ID, 1) if err != nil { t.Fatal(err) } forked, err := s.GetSet(newID) if err != nil { t.Fatal(err) } if forked.ID == created.ID { t.Error("fork should have a new ID") } if forked.Version != 1 { t.Errorf("fork version: want 1, got %d", forked.Version) } if forked.Tag != "" { t.Errorf("fork tag should be cleared, got %q", forked.Tag) } } func TestDeleteSetSoftDeletes(t *testing.T) { s, _ := New(t.TempDir()) created, _ := s.CreateSet(sampleSet(), "") if err := s.Delete(KindSet, created.ID); err != nil { t.Fatal(err) } if _, err := s.GetSet(created.ID); err == nil { t.Error("expected set to be gone after delete") } // Deleting again is a not-found. if err := s.Delete(KindSet, created.ID); err != ErrNotFound { t.Errorf("want ErrNotFound, got %v", err) } } func TestSetValidation(t *testing.T) { s, _ := New(t.TempDir()) bad := ConfigSet{Name: "", Parameters: nil} if _, err := s.CreateSet(bad, ""); err == nil { t.Error("expected empty-name validation error") } dup := ConfigSet{Name: "x", Parameters: []Parameter{ {Key: "a", DS: "d", Signal: "s", Type: TypeFloat}, {Key: "a", DS: "d", Signal: "s2", Type: TypeFloat}, }} if _, err := s.CreateSet(dup, ""); err == nil { t.Error("expected duplicate-key validation error") } defaultOOR := ConfigSet{Name: "x", Parameters: []Parameter{ {Key: "a", DS: "d", Signal: "s", Type: TypeFloat, Default: 100.0, Max: fptr(10)}, }} if _, err := s.CreateSet(defaultOOR, ""); err == nil { t.Error("expected out-of-range default validation error") } } func TestInstanceLifecycle(t *testing.T) { s, _ := New(t.TempDir()) set, _ := s.CreateSet(sampleSet(), "") inst := ConfigInstance{ Name: "nominal", SetID: set.ID, Values: map[string]any{"voltage": 24.0, "enabled": true}, } out, err := s.CreateInstance(inst, "") if err != nil { t.Fatal(err) } if out.Version != 1 { t.Errorf("instance version: want 1, got %d", out.Version) } // Update value -> v2. out.Values["voltage"] = 36.0 v2, err := s.UpdateInstance(out.ID, out, "bump") if err != nil { t.Fatal(err) } if v2.Version != 2 { t.Errorf("instance version after update: want 2, got %d", v2.Version) } } func TestInstanceValidation(t *testing.T) { s, _ := New(t.TempDir()) set, _ := s.CreateSet(sampleSet(), "") // voltage is mandatory with a default, so omitting it is OK; but an // out-of-range value must fail. bad := ConfigInstance{Name: "x", SetID: set.ID, Values: map[string]any{"voltage": 999.0}} if _, err := s.CreateInstance(bad, ""); err == nil { t.Error("expected out-of-range value error") } // Unknown parameter key must fail. unknown := ConfigInstance{Name: "x", SetID: set.ID, Values: map[string]any{"nope": 1.0}} if _, err := s.CreateInstance(unknown, ""); err == nil { t.Error("expected unknown-parameter error") } // Unknown set must fail. missing := ConfigInstance{Name: "x", SetID: "does-not-exist", Values: map[string]any{}} if _, err := s.CreateInstance(missing, ""); err == nil { t.Error("expected missing-set error") } }