B0's shape wins: the running checker asks and records facts, the checking program decides. The registry's types win: the rules and the program read the generated checker contract and the domain SDK, and no mesh-sdk delivery import is left. The registry's series, renames and unparsed seat definitions become facts with their refusals as rules; the tagged versions ride on the case. The program reads the heads' seat definitions through sdk/seatdefs and the running checker lays them over its reader's list by seat, reading no file of the heads: the reader's list is larger than a run input's bound. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
168 lines
8.7 KiB
Go
168 lines
8.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
// 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 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)
|
|
}
|
|
}
|
|
}
|