Files
mesh-delivery/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go
T
jschoubben 9290cdda4d The checking program judges the module graph's cycles from the edges the running checker gathers (issue 1921, B0 review round 1)
ADR 0656 §5 has every check judged by parts built from the change's heads, and no record keeps the cycle rule in the
running checker. The facts carry each ordering edge's two modules and kind, never manifests: the whole catalogue's
graph is 50 edges, 3195 bytes.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 23:28:20 +01:00

142 lines
7.2 KiB
Go

package main
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-checker/rules"
)
// 1921 (B0, D2): the checking program answers, on its last line, the verdict the rules give the facts in its input, and
// refuses facts of a version it does not read, an input it is not given, and a question it is not asked, naming each.
// 1921 (B0, D3): asked -seats, it answers the seat list of the heads laid for it: a definition a head adds, resolved with
// the parts it includes; what it left out, and why; and an empty list for heads with no definition.
func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) {
failing, _ := json.Marshal(rules.Tested{Level: delivery.LevelUnit, Tests: 3, Failed: []rules.Failure{{Package: "x/a", Test: "TestA", Cause: "a_test.go:9: boom"}}})
facts := rules.Facts{V: rules.FactsVersion, Group: "feat/x", Heads: "0123456789ab", Members: []rules.Member{{Repository: "novox/x", Number: 3, Head: "h"}},
Steps: []rules.Step{
{Repository: "novox/x", Number: 3, Module: "mesh-build-go", Node: "laptop", Level: &delivery.LevelAnswer{Level: delivery.LevelUnit, Tests: []json.RawMessage{failing}}},
{Repository: "novox/x", Number: 3, Failed: &delivery.FailedFirst{Machine: "laptop", Words: "novox/x#3 has files in conflict: go.mod"}},
}}
dir := t.TempDir()
write := func(name string, v any) string {
data, _ := json.Marshal(v)
p := filepath.Join(dir, name)
if err := os.WriteFile(p, data, 0o600); err != nil {
t.Fatal(err)
}
return p
}
v1 := write("v1.json", facts)
later := facts
later.V = rules.FactsVersion + 1
v2 := write("v2.json", later)
heads := filepath.Join(dir, "heads")
for p, body := range map[string]string{
"novox/mesh-contracts/contracts/seats/node-build-x/seat.json": `{"format":1,"seat":"node-build-x","scope":"node","description":"kept","includes":["build"],"verbs":[{"name":"own"}]}`,
"novox/mesh-contracts/contracts/parts/build.json": `{"part":"build","verbs":[{"name":"current"}],"events":[{"name":"ended"}]}`,
"novox/mesh-contracts/contracts/seats/broken/seat.json": `{"format":1,"seat":"broken","scope":"nowhere"}`,
} {
if err := os.MkdirAll(filepath.Dir(filepath.Join(heads, p)), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(heads, p), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
none := filepath.Join(dir, "no-heads")
if err := os.MkdirAll(filepath.Join(none, "novox/x"), 0o755); err != nil {
t.Fatal(err)
}
for name, c := range map[string]struct {
args []string
input, heads string
exit int
said string
}{
"the seats": {args: []string{"-seats"}, heads: heads},
"no seats": {args: []string{"-seats"}, heads: none},
"no heads laid": {args: []string{"-seats"}, exit: 2, said: delivery.RunHeadsEnv + " names no directory"},
"the facts": {args: []string{"-verdict"}, input: v1},
"a later version": {args: []string{"-verdict"}, input: v2, exit: 2, said: "facts of version 2, this checking program reads version 1"},
"no input": {args: []string{"-verdict"}, exit: 2, said: delivery.RunInputEnv + " names no file"},
"nothing asked": {input: v1, exit: 2, said: "nothing asked"},
"an unknown asking": {args: []string{"-judge"}, input: v1, exit: 2, said: "-judge"},
} {
var out, errs bytes.Buffer
exit := run(c.args, func(k string) string {
switch k {
case delivery.RunInputEnv:
return c.input
case delivery.RunHeadsEnv:
return c.heads
}
return ""
}, &out, &errs)
if exit != c.exit || !strings.Contains(errs.String(), c.said) {
t.Errorf("%s: exit %d, said %q; want %d and %q", name, exit, errs.String(), c.exit, c.said)
continue
}
if c.exit != 0 {
continue
}
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
if c.heads != "" {
var sa rules.SeatsAnswer
if err := json.Unmarshal([]byte(lines[len(lines)-1]), &sa); err != nil || sa.V != rules.SeatsVersion || sa.Seats == nil {
t.Fatalf("%s: the last line %q: %v", name, lines[len(lines)-1], err)
}
if c.heads == none {
if len(sa.Seats) != 0 || len(sa.Left) != 0 {
t.Errorf("%s: %+v, want an empty list", name, sa)
}
continue
}
want := `{"description":"kept","events":[{"name":"ended"}],"format":1,"scope":"node","seat":"node-build-x","verbs":[{"name":"current"},{"name":"own"}]}`
if len(sa.Seats) != 1 || string(sa.Seats[0]) != want || len(sa.Left) != 1 || !strings.Contains(sa.Left[0].Why, `not "broken" and "nowhere"`) {
t.Errorf("%s: seats %s, left %+v; want %s and broken left out", name, sa.Seats, sa.Left, want)
}
continue
}
var a rules.Answer
if err := json.Unmarshal([]byte(lines[len(lines)-1]), &a); err != nil || a.V != rules.AnswerVersion {
t.Fatalf("%s: the last line %q: %v", name, lines[len(lines)-1], err)
}
if want := rules.Judge(facts); !reflect.DeepEqual(a.Verdict, want) || a.Verdict.State != delivery.VerdictFail || len(a.Verdict.Failures) != 2 ||
a.Verdict.Failures[1].Words != "novox/x#3 has files in conflict: go.mod" || a.Verdict.Machines[0] != "laptop" || a.Verdict.Levels[delivery.LevelUnit].Run != 3 {
t.Errorf("%s: the verdict %+v, want %+v", name, a.Verdict, want)
}
} // Review round 1 of milestone 47/B0: the module graph's cycle rule is the program's (ADR 0603 §2, ADR 0656 §5), over the
// edges the running checker gathers. A cycle the change makes fails; one the trunk has among the same modules is noted.
web, mail := rules.Edge{From: "web", To: "mail", Kind: rules.EdgeAwaits}, rules.Edge{From: "mail", To: "web", Kind: rules.EdgeAwaits}
only := func(e rules.Edge, where string) rules.Edge { e.Only = where; return e }
for _, row := range []struct {
name string
graph rules.Graph
fails string
note string
}{
{name: "no cycle", graph: rules.Graph{Trunk: true, Edges: []rules.Edge{web}}, note: "has no cycle"},
{name: "a cycle the change makes", graph: rules.Graph{Trunk: true, Edges: []rules.Edge{mail, only(web, rules.EdgeChange)}}, fails: "has the cycle mail -> web -> mail"},
{name: "a cycle the trunk has", graph: rules.Graph{Trunk: true, Edges: []rules.Edge{mail, web}}, note: "which the trunk already has"},
{name: "the trunk unread", graph: rules.Graph{Edges: []rules.Edge{mail, web}}, fails: "has the cycle mail -> web -> mail"},
{name: "a base on itself", graph: rules.Graph{Trunk: true, Edges: []rules.Edge{only(rules.Edge{From: "web", To: "web", Kind: rules.EdgeBase}, rules.EdgeChange)}}, fails: "has the cycle web -> web"},
{name: "an edge only the trunk has", graph: rules.Graph{Trunk: true, Edges: []rules.Edge{mail, only(web, rules.EdgeTrunk)}}, note: "has no cycle"},
} {
g := row.graph
a := rules.Judged(rules.Facts{V: rules.FactsVersion, Steps: []rules.Step{{Graph: &g}}})
if row.fails != "" && (a.Verdict.State != delivery.VerdictFail || !strings.Contains(a.Verdict.Failed.Words, row.fails)) {
t.Errorf("%s: verdict %+v, want a fail saying %q", row.name, a.Verdict, row.fails)
}
if row.note != "" && (a.Verdict.State != delivery.VerdictPass || !strings.Contains(strings.Join(a.Notes, "\n"), row.note)) {
t.Errorf("%s: verdict %s, notes %q, want a pass noting %q", row.name, a.Verdict.State, a.Notes, row.note)
}
}
}