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) } } }