Files
mesh-delivery/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go
T
jschoubben 932e7ccf02
mesh/delivery-group group feat/registry-milestone checking: 7 of 7 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check failed (3): ace: the check of novox/mesh-catalog#612 failed: the unit level: the candidate's mes…; and 2 more | 932e7ccf0255 on bab05c66b3cb
A checker that hands no seat digests gets the heads' definitions with only each seat's own description (issue 1921, milestone 47/G, review round 2)
G's own check is made by the running B0 checker, which hands no digests, so the
program answers every head definition: 342 KB, over the 256 KiB a run's answer may
be. Without digests the descriptions of verbs, events and their schemas are left
out (171 KB for the group's heads); neither the manifest validator nor the composer
reads one, and the validator's verdicts over the group are the same either way.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-07 03:41:55 +01:00

256 lines
13 KiB
Go

package main
import (
"bytes"
"encoding/json"
"io/fs"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"mesh-delivery-checker/rules"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// 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: buildstep.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: &buildstep.LevelAnswer{Level: buildstep.LevelUnit, Tests: []json.RawMessage{failing}}},
{Repository: "novox/x", Number: 3, Failed: &rules.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)
// The heads as the build node lays them, each at its repository's path: a contracts module of its own defining a seat
// with a part, and the mesh's own contracts module defining a seat of a domain that is not its (ADR 0636 §5).
lay := func(name string, files map[string]string) string {
root := filepath.Join(dir, name)
for p, body := range files {
if err := os.MkdirAll(filepath.Dir(filepath.Join(root, p)), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, p), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
return root
}
contracts := func(module string) string {
return `{"module": "` + module + `", "build": {"artifacts": [{"name": "go", "kind": "package", "language": "go", "generated-from": "."}]}}`
}
heads := lay("heads", map[string]string{
"novox/x/x-contracts/module.json": contracts("x-contracts"),
"novox/x/x-contracts/seats/node-build-x/seat.json": `{"format":1,"seat":"node-build-x","scope":"node","grain":"whole","domain":"Delivery","decision":"ADR 1","description":"kept","includes":["build"],"verbs":[{"name":"own","input":{"type":"object"},"answer":{"type":"object"}}]}`,
"novox/x/x-contracts/parts/build.json": `{"format":1,"part":"build","verbs":[{"name":"current","input":{"type":"object"},"answer":{"type":"object"}}],"events":[{"name":"ended","body":{"type":"object"}}]}`,
"novox/x/contracts/module.json": contracts("mesh-contracts"),
"novox/x/contracts/seats/mesh-catalog/seat.json": `{"format":1,"seat":"mesh-catalog","scope":"mesh","grain":"whole","domain":"Delivery","decision":"ADR 1"}`,
})
broken := lay("broken", map[string]string{
"novox/x/x-contracts/module.json": contracts("x-contracts"),
"novox/x/x-contracts/seats/broken/seat.json": `{"format":1,"seat":"broken","scope":"nowhere"}`,
})
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"}, input: v1, heads: heads},
"no seats": {args: []string{"-seats"}, input: v1, heads: none},
"a seat unparsed": {args: []string{"-seats"}, input: v1, heads: broken, exit: 2, said: "the seat definition of broken (module x-contracts) at the head of novox/x is not taken"},
"no heads laid": {args: []string{"-seats"}, exit: 2, said: buildstep.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: buildstep.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 buildstep.RunInputEnv:
return c.input
case buildstep.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
}
var d struct {
Seat string `json:"seat"`
Verbs []struct {
Name string `json:"name"`
} `json:"verbs"`
}
if len(sa.Seats) == 1 {
_ = json.Unmarshal(sa.Seats[0], &d)
}
if d.Seat != "node-build-x" || len(d.Verbs) != 2 || len(sa.Left) != 1 || !strings.Contains(sa.Left[0].Why, "whose seats mesh-delivery-contracts defines") {
t.Errorf("%s: seats %s, left %+v; want node-build-x with its part's verb, and mesh-catalog left out", name, sa.Seats, sa.Left)
}
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 != meshdeliverycheckerclient.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[buildstep.LevelUnit].Run != 3 {
t.Errorf("%s: the verdict %+v, want %+v", name, a.Verdict, want)
}
}
// Review finding 3 of milestone 47/G: the heads of a real group carry every seat definition the reader holds, unchanged
// (this repository's own contracts, laid as its head): the answer leaves them all to the checker's list and stays
// within the bound a run's answer is written under (buildstep.RunInputBound), where the whole list would not.
t.Run("heads carrying every definition the reader holds unchanged answer none of them, within the bound", func(t *testing.T) {
real := under{prefix: "novox/mesh-delivery", fs: os.DirFS("../../../..")}
group := rules.Facts{V: rules.FactsVersion, Members: []rules.Member{{Repository: "novox/mesh-delivery", Head: "h"}}}
// An empty list of digests answers every definition whole (a checker that hands its list's digests, holding none).
whole, err := rules.SeatsOf(real, rules.Facts{V: rules.FactsVersion, Members: group.Members, SeatDigests: map[string]string{}})
if err != nil || len(whole.Seats) < 10 {
t.Fatalf("the repository's own definitions: %d, %v", len(whole.Seats), err)
}
group.SeatDigests = map[string]string{}
for _, raw := range whole.Seats {
var d struct{ Seat string }
_ = json.Unmarshal(raw, &d)
group.SeatDigests[d.Seat] = rules.SeatDigest(raw)
}
same, err := rules.SeatsOf(real, group)
answer, _ := json.Marshal(same)
if err != nil || len(same.Seats) != 0 || same.Same != len(whole.Seats) || len(answer) > buildstep.RunInputBound {
t.Errorf("unchanged heads answer %d definitions (%d the same), %d bytes; want none, all %d the same, within %d: %v",
len(same.Seats), same.Same, len(answer), len(whole.Seats), buildstep.RunInputBound, err)
}
// Review round 2 of G: a checker from before G (B0's, which makes G's own check) hands no digests, and the answer
// then keeps each seat's own description only, so a group's whole list stays within the bound; each still parses.
b0, err := rules.SeatsOf(real, rules.Facts{V: rules.FactsVersion, Members: group.Members})
if err != nil || len(b0.Seats) != len(whole.Seats) {
t.Fatalf("B0-shaped facts: %d definitions, %v", len(b0.Seats), err)
}
for i, raw := range b0.Seats {
var own struct {
Description string `json:"description"`
}
_ = json.Unmarshal(whole.Seats[i], &own)
var d struct {
Description string `json:"description"`
Verbs []json.RawMessage `json:"verbs"`
Events []json.RawMessage `json:"events"`
}
_ = json.Unmarshal(raw, &d)
inner := 0
for _, m := range append(d.Verbs, d.Events...) {
inner += strings.Count(string(m), `"description":"`)
}
if _, perr := seatdef.Parse(raw); inner > 0 || d.Description != own.Description || perr != nil {
t.Errorf("B0-shaped facts answer %s with %d inner descriptions, its own %q: %v; want only its own", raw[:min(len(raw), 80)], inner, d.Description, perr)
}
}
if answer, _ := json.Marshal(b0); len(answer) > buildstep.RunInputBound || len(answer) >= len(mustJSON(t, whole))*3/4 {
t.Errorf("B0-shaped facts answer %d bytes (whole %d), want within %d and far below the whole", len(answer), len(mustJSON(t, whole)), buildstep.RunInputBound)
}
// One definition the heads change is answered alone.
for seat := range group.SeatDigests {
group.SeatDigests[seat] = "changed"
break
}
if one, err := rules.SeatsOf(real, group); err != nil || len(one.Seats) != 1 {
t.Errorf("one changed definition: answered %d, %v", len(one.Seats), err)
}
})
// 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 != meshdeliverycheckerclient.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 != meshdeliverycheckerclient.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)
}
}
}
// under is a directory laid at a repository's path of the heads, as the build node lays a head.
type under struct {
prefix string
fs fs.FS
}
func (u under) Open(name string) (fs.File, error) {
if name == u.prefix {
return u.fs.Open(".")
}
if rest, ok := strings.CutPrefix(name, u.prefix+"/"); ok {
return u.fs.Open(rest)
}
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
func mustJSON(t *testing.T, v any) []byte {
t.Helper()
out, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
return out
}