diff --git a/go/delivery/checker.go b/go/delivery/checker.go new file mode 100644 index 0000000..bdb57e9 --- /dev/null +++ b/go/delivery/checker.go @@ -0,0 +1,295 @@ +package delivery + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "time" +) + +// The checker's keys (novox/hq ADR 0447 clauses 1 to 3, ADR 0444, ADR 0446 clause 5, ADR 0453 clause 4): the verdicts of +// its checks and the passing verdicts it keeps per package and level. The checker writes them from R4c, dark; the +// forge bridge writes the status `mesh/check` from a verdict (unrequired until R4f), the gate reads it from R4f, and +// the view and the watchman read them (a running check carries a pulse). +// +// **The verdict's key is the group and the heads, not the fingerprints** (the lead's limit for R4a). Research 063 +// section 01 drew `verdicts..check.`; ADR 0447 clause 1 judges a pull request's head merged onto its +// trunk's head, and a repository without modules has no fingerprint set, so the key is `.` as the +// heads hash of HeadsHash gives it. The fpset (FingerprintSet) is not part of it. +const ( + // CheckerModule is the module of the checker part. + CheckerModule = "mesh-delivery-checker" + // CheckerVerdictsBucket is the checker's bucket of verdicts, `_`. + CheckerVerdictsBucket = CheckerModule + "_verdicts" + // CheckerVerdictsHistory is the revisions the verdicts bucket keeps per key. + CheckerVerdictsHistory = 5 + // CheckerKeptBucket is the checker's bucket of kept verdicts. A key is written once, by a create that fails when + // the key exists, by the checker's credential alone, from the result the checker reads from the test run and + // never from anything the code under test writes (ADR 0444 clause 5); nothing changes it afterwards. + CheckerKeptBucket = CheckerModule + "_kept" + // CheckerKeptHistory is the revisions the kept bucket keeps per key. + CheckerKeptHistory = 1 +) + +// The test levels (ADR 0445 clause 2) and the states of a verdict. +const ( + LevelUnit = "unit" + LevelIntegration = "integration" + + VerdictRunning = "running" + VerdictPass = "pass" + VerdictFail = "fail" +) + +func checkLevel(level string) error { + if level != LevelUnit && level != LevelIntegration { + return fmt.Errorf("delivery: the level %q is not %s or %s", level, LevelUnit, LevelIntegration) + } + return nil +} + +func isHex12(s string) bool { return len(s) == 12 && strings.Trim(s, "0123456789abcdef") == "" } + +// CheckerVerdictKey is the key, in CheckerVerdictsBucket, of a group's check at its heads: `.`, the group escaped as CandidatesKey does and the heads hash as HeadsHash gives it (12 lowercase hex digits). +func CheckerVerdictKey(group, headsHash string) (string, error) { + g, err := groupKey(group) + if err != nil { + return "", err + } + if !isHex12(headsHash) { + return "", fmt.Errorf("delivery: %q is not a heads hash, 12 lowercase hex digits", headsHash) + } + return g + "." + headsHash, nil +} + +// ParseCheckerVerdictKey is the inverse of CheckerVerdictKey. +func ParseCheckerVerdictKey(key string) (group, headsHash string, err error) { + i := strings.LastIndex(key, ".") + if i < 0 { + return "", "", fmt.Errorf("delivery: %q is not a verdict key, .", key) + } + part, headsHash := key[:i], key[i+1:] + if !isHex12(headsHash) { + return "", "", fmt.Errorf("delivery: %q is not a verdict key: %q is not a heads hash, 12 lowercase hex digits", key, headsHash) + } + if group, err = parseGroupKey(part); err != nil { + return "", "", fmt.Errorf("delivery: %q is not a verdict key: %w", key, err) + } + if strings.Contains(part, ".") { + return "", "", fmt.Errorf("delivery: %q is not a verdict key: the group part holds a dot", key) + } + return group, headsHash, nil +} + +// LevelResult is a level's tests: how many ran in this check and how many were taken from kept verdicts (ADR 0444 +// clause 2). +type LevelResult struct { + Run int `json:"run"` + Kept int `json:"kept"` +} + +// Took is the time of each part of a check in integer milliseconds (ADR 0447 clause 3): waiting in the queue, cloning, +// the unit level, the integration level's setup and its running, and the composition. +type Took struct { + Queue int64 `json:"queue"` + Clone int64 `json:"clone"` + Unit int64 `json:"unit"` + IntegrationSetup int64 `json:"integration-setup"` + IntegrationRun int64 `json:"integration-run"` + Composition int64 `json:"composition"` +} + +// FailedFirst names what failed first: a test (its package and name) or a machine (and the words of the composition's +// refusal); one of them, with Words optional. +type FailedFirst struct { + Test string `json:"test,omitempty"` + Machine string `json:"machine,omitempty"` + Words string `json:"words,omitempty"` +} + +// Verdict is the value of a verdict key: the group and its heads hash, the trunk head of each repository it was judged +// against (repository to full sha), the State (running, pass or fail), what failed first, the tests run and kept per +// level, each part's time, the machines composed and the mesh-sdk version the composition ran with (ADR 0446 clause 5). +type Verdict struct { + V int `json:"v"` + Group string `json:"group"` + Heads string `json:"heads"` + Trunk map[string]string `json:"trunk,omitempty"` + State string `json:"state"` + Failed FailedFirst `json:"failed,omitzero"` + Levels map[string]LevelResult `json:"levels,omitempty"` + Took Took `json:"took,omitzero"` + Machines []string `json:"machines,omitempty"` + SDK string `json:"sdk,omitempty"` +} + +func (v Verdict) check() error { + if v.Group == "" { + return fmt.Errorf("delivery: a verdict names no group") + } + if !isHex12(v.Heads) { + return fmt.Errorf("delivery: the verdict's heads hash %q is not 12 lowercase hex digits", v.Heads) + } + for repository, head := range v.Trunk { + if head == "" { + return fmt.Errorf("delivery: the verdict's trunk head of %s is empty", repository) + } + } + for level, r := range v.Levels { + if err := checkLevel(level); err != nil { + return err + } + if r.Run < 0 || r.Kept < 0 { + return fmt.Errorf("delivery: the verdict's %s level counts a negative number of tests", level) + } + } + for part, ms := range map[string]int64{"queue": v.Took.Queue, "clone": v.Took.Clone, "unit": v.Took.Unit, + "integration-setup": v.Took.IntegrationSetup, "integration-run": v.Took.IntegrationRun, "composition": v.Took.Composition} { + if ms < 0 { + return fmt.Errorf("delivery: the verdict's time of the part %s is negative", part) + } + } + named := v.Failed.Test != "" || v.Failed.Machine != "" + switch v.State { + case VerdictRunning: + if v.Failed != (FailedFirst{}) { + return fmt.Errorf("delivery: a running verdict names no failure") + } + case VerdictPass: + if v.Failed != (FailedFirst{}) { + return fmt.Errorf("delivery: a pass names no failure") + } + for _, level := range []string{LevelUnit, LevelIntegration} { + if _, ok := v.Levels[level]; !ok { + return fmt.Errorf("delivery: a pass says nothing of the %s level", level) + } + } + case VerdictFail: + if !named { + return fmt.Errorf("delivery: a fail names neither a test nor a machine") + } + default: + return fmt.Errorf("delivery: the verdict state %q is not running, pass or fail", v.State) + } + return nil +} + +// EncodeVerdict is v as JSON at Version; it refuses a fail that names neither a test nor a machine, a pass without both +// levels, an unknown state or level, and a negative count or time. +func EncodeVerdict(v Verdict) ([]byte, error) { + if err := v.check(); err != nil { + return nil, err + } + v.V = Version + return json.Marshal(v) +} + +// DecodeVerdict reads a verdict; it refuses an unknown version (ErrUnknownVersion) and what EncodeVerdict refuses. +func DecodeVerdict(data []byte) (Verdict, error) { + var v Verdict + if err := decode(data, &v); err != nil { + return Verdict{}, err + } + if err := v.check(); err != nil { + return Verdict{}, err + } + return v, nil +} + +// SameVerdict says whether two verdicts are the same value, so the checker writes only when they differ. +func SameVerdict(a, b Verdict) bool { return reflect.DeepEqual(a, b) } + +// KeptKey is the key, in CheckerKeptBucket, of a kept verdict: `.`, the level unit or integration (ADR +// 0445 clause 2) and the digest, lowercase hex, of everything that package's tests read at that level as the +// language's build module names it (ADR 0444 clause 1, ADR 0445 clause 5). +func KeptKey(level, digest string) (string, error) { + if err := checkLevel(level); err != nil { + return "", err + } + if digest == "" || strings.Trim(digest, "0123456789abcdef") != "" { + return "", fmt.Errorf("delivery: %q is not a digest, lowercase hex", digest) + } + return level + "." + digest, nil +} + +// ParseKeptKey is the inverse of KeptKey. +func ParseKeptKey(key string) (level, digest string, err error) { + level, digest, ok := strings.Cut(key, ".") + if !ok { + return "", "", fmt.Errorf("delivery: %q is not a kept key, .", key) + } + if _, err = KeptKey(level, digest); err != nil { + return "", "", fmt.Errorf("delivery: %q is not a kept key: %w", key, err) + } + return level, digest, nil +} + +// Kept is the value of a kept key: a passing verdict of one package at one level (State is pass and nothing else: a +// failure is never kept, ADR 0444 clause 1), with the repository, the package, the number of tests, the time they took +// in milliseconds, the toolchain's version and when it was kept. +type Kept struct { + V int `json:"v"` + State string `json:"state"` + Repository string `json:"repository"` + Package string `json:"package"` + Level string `json:"level"` + Tests int `json:"tests"` + Took int64 `json:"took"` + Toolchain string `json:"toolchain"` + At time.Time `json:"at"` +} + +func (k Kept) check() error { + if k.State != VerdictPass { + return fmt.Errorf("delivery: a kept verdict is a pass, never %q: a failure is never kept", k.State) + } + if err := checkRepository(k.Repository); err != nil { + return err + } + if k.Package == "" { + return fmt.Errorf("delivery: a kept verdict names no package") + } + if err := checkLevel(k.Level); err != nil { + return err + } + if k.Tests < 0 || k.Took < 0 { + return fmt.Errorf("delivery: a kept verdict counts a negative number of tests or a negative time") + } + if k.Toolchain == "" { + return fmt.Errorf("delivery: a kept verdict names no toolchain version") + } + if k.At.IsZero() { + return fmt.Errorf("delivery: a kept verdict says no time") + } + return nil +} + +// EncodeKept is k as JSON at Version; it refuses a kept value that is not a pass. +func EncodeKept(k Kept) ([]byte, error) { + if err := k.check(); err != nil { + return nil, err + } + k.V = Version + return json.Marshal(k) +} + +// DecodeKept reads a kept value; it refuses an unknown version (ErrUnknownVersion) and what EncodeKept refuses. +func DecodeKept(data []byte) (Kept, error) { + var k Kept + if err := decode(data, &k); err != nil { + return Kept{}, err + } + if err := k.check(); err != nil { + return Kept{}, err + } + return k, nil +} + +// SameKept says whether two kept values are the same value. +func SameKept(a, b Kept) bool { + at := a.At.Equal(b.At) + a.At, b.At = time.Time{}, time.Time{} + return at && a == b +} diff --git a/go/delivery/checker_test.go b/go/delivery/checker_test.go new file mode 100644 index 0000000..2dd939f --- /dev/null +++ b/go/delivery/checker_test.go @@ -0,0 +1,222 @@ +package delivery + +import ( + "errors" + "strings" + "testing" + "time" +) + +func passVerdict() Verdict { + return Verdict{ + Group: "feat/x", + Heads: "4b5491557853", + Trunk: map[string]string{"novox/mesh-sdk": "aaaa", "novox/mesh-control": "bbbb"}, + State: VerdictPass, + Levels: map[string]LevelResult{LevelUnit: {Run: 12, Kept: 301}, LevelIntegration: {Run: 3, Kept: 40}}, + Took: Took{Queue: 1200, Clone: 800, Unit: 950, IntegrationSetup: 2100, IntegrationRun: 7400, Composition: 3300}, + Machines: []string{"ace", "g14", "novox", "shanks"}, + SDK: "v0.1.14-0.20261019091011-aa68b67389c9", + } +} + +func verdictSamples() map[string]Verdict { + running := Verdict{Group: "feat/x", Heads: "4b5491557853", Trunk: map[string]string{"novox/mesh-sdk": "aaaa"}, State: VerdictRunning} + failTest := passVerdict() + failTest.State, failTest.Failed = VerdictFail, FailedFirst{Test: "novox/mesh-sdk/go/delivery TestHeadsHash"} + failTest.Levels = map[string]LevelResult{LevelUnit: {Run: 1}} + failMachine := passVerdict() + failMachine.State, failMachine.Failed = VerdictFail, FailedFirst{Machine: "ace", Words: "path /etc/x is owned by two modules"} + return map[string]Verdict{"running": running, "pass": passVerdict(), "fail by a test": failTest, "fail by a machine": failMachine} +} + +func keptSample() Kept { + return Kept{State: VerdictPass, Repository: "novox/mesh-sdk", Package: "git.novox.be/novox/mesh-sdk/go/delivery", Level: LevelUnit, + Tests: 41, Took: 190, Toolchain: "1.26.0", At: time.Date(2026, 10, 20, 2, 0, 0, 0, time.UTC)} +} + +// 1359/AC1: the key is .; parsing gives back the group and the hash. +func TestCheckerVerdictKey(t *testing.T) { + heads := []Head{{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}} + hh, _ := HeadsHash(heads) + for _, group := range []string{"feat/x", "fix/1.2", "plain", "odd name=1"} { + key, err := CheckerVerdictKey(group, hh) + if err != nil { + t.Fatal(err) + } + if want := EscapeName(group) + "." + hh; key != want { + t.Errorf("key = %q, want %q", key, want) + } + g, h, err := ParseCheckerVerdictKey(key) + if err != nil || g != group || h != hh { + t.Errorf("parse(%q) = %q, %q, %v", key, g, h, err) + } + } + if k, _ := CheckerVerdictKey("feat/x", hh); !strings.HasPrefix(k, "feat/x.") { + t.Errorf("a group with a slash is not kept readable: %q", k) + } + if _, err := CheckerVerdictKey("", hh); err == nil { + t.Error("an empty group accepted") + } + for _, bad := range []string{"", "feat/x", "feat/x.", "feat/x.abc", "feat/x." + strings.ToUpper(hh), ".4b5491557853", "a.b.4b5491557853", "feat/x.4b5491557853a"} { + if _, _, err := ParseCheckerVerdictKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { + t.Errorf("ParseCheckerVerdictKey(%q) = %v, want a refusal naming it", bad, err) + } + } + if _, err := CheckerVerdictKey("feat/x", "abc"); err == nil || !strings.Contains(err.Error(), "abc") { + t.Errorf("a short heads hash: %v", err) + } +} + +// 1359/AC2: a verdict in each state round-trips, with times per part in milliseconds, the tests run and kept per level, +// the machines composed and the mesh-sdk version composed with. +func TestVerdictRoundTrip(t *testing.T) { + for name, v := range verdictSamples() { + data, err := EncodeVerdict(v) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + if !strings.HasPrefix(string(data), `{"v":1,`) { + t.Errorf("%s at %s", name, data) + } + got, err := DecodeVerdict(data) + v.V = Version + if err != nil || !SameVerdict(got, v) { + t.Errorf("%s: %+v %v", name, got, err) + } + } + data, _ := EncodeVerdict(passVerdict()) + for _, field := range []string{`"queue":1200`, `"clone":800`, `"unit":950`, `"integration-setup":2100`, `"integration-run":7400`, `"composition":3300`, `"run":12`, `"kept":301`, `"machines":["ace","g14","novox","shanks"]`, `"sdk":"v0.1.14`} { + if !strings.Contains(string(data), field) { + t.Errorf("the value lacks %s: %s", field, data) + } + } +} + +// 1359/AC3: a fail that names neither a test nor a machine, and a pass whose levels are missing, are refused. +func TestVerdictChecked(t *testing.T) { + cases := map[string]struct { + edit func(*Verdict) + words []string + }{ + "fail naming nothing": {func(v *Verdict) { v.State, v.Failed = VerdictFail, FailedFirst{} }, []string{"test", "machine"}}, + "fail with words only": {func(v *Verdict) { v.State, v.Failed = VerdictFail, FailedFirst{Words: "x"} }, []string{"test", "machine"}}, + "pass without levels": {func(v *Verdict) { v.Levels = nil }, []string{"level"}}, + "pass without unit": {func(v *Verdict) { delete(v.Levels, LevelUnit) }, []string{"unit"}}, + "pass without integration": {func(v *Verdict) { delete(v.Levels, LevelIntegration) }, []string{"integration"}}, + "pass naming a failure": {func(v *Verdict) { v.Failed = FailedFirst{Test: "T"} }, []string{"pass"}}, + "an unknown state": {func(v *Verdict) { v.State = "maybe" }, []string{"maybe"}}, + "an unknown level": {func(v *Verdict) { v.Levels["e2e"] = LevelResult{} }, []string{"e2e"}}, + "a negative time": {func(v *Verdict) { v.Took.Clone = -1 }, []string{"clone"}}, + "a bad heads hash": {func(v *Verdict) { v.Heads = "xyz" }, []string{"xyz"}}, + "no group": {func(v *Verdict) { v.Group = "" }, []string{"group"}}, + "a trunk head left empty": {func(v *Verdict) { v.Trunk["novox/mesh-sdk"] = "" }, []string{"novox/mesh-sdk"}}, + "a negative count": {func(v *Verdict) { v.Levels[LevelUnit] = LevelResult{Run: -1} }, []string{"unit"}}, + } + for name, c := range cases { + v := passVerdict() + c.edit(&v) + _, err := EncodeVerdict(v) + if err == nil { + t.Errorf("%s accepted", name) + continue + } + for _, w := range c.words { + if !strings.Contains(err.Error(), w) { + t.Errorf("%s: error %q does not name %q", name, err, w) + } + } + } + if _, err := DecodeVerdict([]byte(`{"v":1,"group":"g","heads":"4b5491557853","state":"fail"}`)); err == nil { + t.Error("decoded a fail naming nothing") + } +} + +// 1359/AC4: the kept key is .; a level other than unit or integration, or a digest that is not hex, +// is refused, naming it. +func TestKeptKey(t *testing.T) { + digest := strings.Repeat("ab12", 16) + for _, level := range []string{LevelUnit, LevelIntegration} { + key, err := KeptKey(level, digest) + if err != nil || key != level+"."+digest { + t.Fatalf("key = %q, %v", key, err) + } + l, d, err := ParseKeptKey(key) + if err != nil || l != level || d != digest { + t.Errorf("parse = %q, %q, %v", l, d, err) + } + } + if _, err := KeptKey("e2e", digest); err == nil || !strings.Contains(err.Error(), "e2e") { + t.Errorf("a level e2e: %v", err) + } + if _, err := KeptKey("", digest); err == nil { + t.Error("an empty level accepted") + } + for _, d := range []string{"", "xyz", "ABCD", "ab 12", "sha256:abcd", "ab12.cd"} { + if _, err := KeptKey(LevelUnit, d); err == nil || !strings.Contains(err.Error(), "\""+d+"\"") { + t.Errorf("KeptKey(unit, %q) = %v, want a refusal naming it", d, err) + } + } + for _, bad := range []string{"", "unit", "unit.", "e2e." + digest, "unit.xyz", "unit." + digest + ".x", "." + digest} { + if _, _, err := ParseKeptKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { + t.Errorf("ParseKeptKey(%q) = %v, want a refusal naming it", bad, err) + } + } + if CheckerVerdictsBucket != "mesh-delivery-checker_verdicts" || CheckerVerdictsHistory != 5 || + CheckerKeptBucket != "mesh-delivery-checker_kept" || CheckerKeptHistory != 1 || CheckerModule != "mesh-delivery-checker" { + t.Errorf("buckets %s %d %s %d", CheckerVerdictsBucket, CheckerVerdictsHistory, CheckerKeptBucket, CheckerKeptHistory) + } +} + +// 1359/AC5: a kept value that is not a pass cannot be encoded; a failure is never kept (ADR 0444 clause 1). +func TestAFailureIsNeverKept(t *testing.T) { + data, err := EncodeKept(keptSample()) + if err != nil { + t.Fatal(err) + } + got, err := DecodeKept(data) + want := keptSample() + want.V = Version + if err != nil || !SameKept(got, want) { + t.Errorf("round trip: %+v %v", got, err) + } + for _, state := range []string{VerdictFail, VerdictRunning, "", "skipped"} { + k := keptSample() + k.State = state + if _, err := EncodeKept(k); err == nil || !strings.Contains(err.Error(), "pass") { + t.Errorf("a kept value in state %q: %v", state, err) + } + } + if _, err := DecodeKept([]byte(`{"v":1,"state":"fail","repository":"novox/mesh-sdk","package":"p","level":"unit","tests":1,"took":1,"toolchain":"1.26.0","at":"2026-10-20T02:00:00Z"}`)); err == nil { + t.Error("decoded a kept failure") + } + for name, edit := range map[string]func(*Kept){ + "a level e2e": func(k *Kept) { k.Level = "e2e" }, + "no package": func(k *Kept) { k.Package = "" }, + "a bad repository": func(k *Kept) { k.Repository = "nope" }, + "no toolchain": func(k *Kept) { k.Toolchain = "" }, + "no time": func(k *Kept) { k.At = time.Time{} }, + "negative tests": func(k *Kept) { k.Tests = -1 }, + "a negative took": func(k *Kept) { k.Took = -1 }, + } { + k := keptSample() + edit(&k) + if _, err := EncodeKept(k); err == nil { + t.Errorf("%s accepted", name) + } + } +} + +// 1359/AC6: a value of another version, or none, is refused with ErrUnknownVersion. +func TestCheckerVersions(t *testing.T) { + for name, dec := range map[string]func([]byte) error{ + "verdict": func(b []byte) error { _, err := DecodeVerdict(b); return err }, + "kept": func(b []byte) error { _, err := DecodeKept(b); return err }, + } { + for _, in := range []string{`{"v":2,"state":"pass"}`, `{"state":"pass"}`} { + if err := dec([]byte(in)); !errors.Is(err, ErrUnknownVersion) { + t.Errorf("%s decode of %s = %v", name, in, err) + } + } + } +}