package main import ( "errors" "strings" "testing" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // **The mesh's module graph decides what a pull request's check runs**, not the repository (novox/hq // ADR 0237 as amended): a change is mapped onto the graph by the rule a merge is (issue 278), the gate // runs when it touches a module — a new one included — and a repository that is the mesh's and touches // none still has its own merge-check.sh run. func TestThePullRequestIsMappedOntoTheModuleGraph(t *testing.T) { const catalogue = "http://forge.internal:20000/novox/mesh-catalog.git" const controller = "http://forge.internal:20000/novox/mesh-controller.git" const host = "http://forge.internal:20000/novox/mesh-host.git" photos := fromRepo("photos", "http://forge.internal:20000/novox/photos.git", "") photos.Source.Ref = "nox-mesh" snake := fromRepo("snake", "jschoubben/snake", "") snake.Source.Seat = "git" entries := []inventory.Entry{ fromRepo("gitea", catalogue, "modules/gitea"), fromRepo("keycloak", catalogue, "modules/keycloak"), fromRepo("nats", catalogue, "modules/nats"), fromRepo("mesh-controller", controller, ""), fromRepo("mesh-host", host, ""), photos, snake, } pull := func(owner, repo, base string, paths []string, dirs ...string) link.PullUpdated { return link.PullUpdated{Owner: owner, Repo: repo, Base: base, Commit: "abc", Paths: paths, ModuleDirs: dirs, ModuleDirsSaid: true} } for _, c := range []struct { what string p link.PullUpdated modules, new, manifest string mesh bool judge string }{ {"one module's own files", pull("novox", "mesh-catalog", "main", []string{"modules/gitea/index.ts"}, "modules/gitea"), "gitea", "", "modules/gitea/module.json", true, ""}, {"a file no module's build reads touches no module (issue 280)", pull("novox", "mesh-catalog", "main", []string{"merge-check.sh", "README.md"}), "", "", "", true, ""}, {"files the announcer could not list: everything built from it", link.PullUpdated{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "abc", PathsTruncated: true, Paths: []string{"README.md"}}, "gitea,keycloak,nats", "", "modules/gitea/module.json,modules/keycloak/module.json,modules/nats/module.json", true, ""}, {"a new module, said by the head", pull("novox", "mesh-catalog", "main", []string{"modules/newmod/index.ts", "modules/newmod/module.json"}, "modules/newmod"), "", "modules/newmod", "modules/newmod/module.json", true, ""}, {"the controller judges itself", pull("novox", "mesh-controller", "main", []string{"cmd/x.go"}), "mesh-controller", "", "module.json", true, "self"}, {"the node-engine is judged with its validator", pull("novox", "mesh-host", "main", []string{"validate/v.go"}), "mesh-host", "", "module.json", true, "validator"}, {"the core's owner, no module: the mesh's, its own check alone", pull("novox", "hq", "main", []string{"README.md"}), "", "", "", true, ""}, {"a branch nothing is built from: the mesh's repository, no module", pull("novox", "photos", "master", []string{"server/x.js"}), "", "", "", true, ""}, {"the branch a module is built from", pull("novox", "photos", "nox-mesh", []string{"server/x.js"}), "photos", "", "module.json", true, ""}, {"a repository on the forge's seat", pull("jschoubben", "snake", "main", []string{"index.html"}), "snake", "", "module.json", true, ""}, {"a repository of nobody's, touching nothing", pull("someone", "dotfiles", "main", []string{"x"}), "", "", "", false, ""}, {"a repository adding a module at its root", pull("someone", "newapp", "main", []string{"module.json", "x.js"}), "", ".", "module.json", true, ""}, } { s := pullScope(c.p, entries, nil, nil) got := []string{strings.Join(s.Modules, ","), strings.Join(s.New, ","), strings.Join(s.Manifests, ",")} want := []string{c.modules, c.new, c.manifest} for i, what := range []string{"modules", "new", "manifests"} { if got[i] != want[i] { t.Errorf("%s: %s %q, wanted %q", c.what, what, got[i], want[i]) } } if s.Mesh != c.mesh || s.Judge != c.judge { t.Errorf("%s: the mesh's %v judged by %q, wanted %v by %q", c.what, s.Mesh, s.Judge, c.mesh, c.judge) } if s.gated() != (c.modules != "" || c.new != "") { t.Errorf("%s: gated %v", c.what, s.gated()) } } } // A module whose build packages another repository's source is touched by a change to it. func TestAPullRequestTouchesWhatPackagesItsRepository(t *testing.T) { entries := []inventory.Entry{fromRepo("node-tools", "http://forge.internal:20000/novox/mesh-tools.git", "node-tools")} read := map[string][]inventory.ReadRepository{"node-tools": {{Repository: "http://forge.internal:20000/novox/mesh-sdk.git"}}} s := pullScope(link.PullUpdated{Owner: "novox", Repo: "mesh-sdk", Base: "main", Commit: "abc", Paths: []string{"go/x.go"}}, entries, read, nil) if strings.Join(s.Modules, ",") != "node-tools" || len(s.Manifests) != 0 { t.Fatalf("a change to what node-tools packages touched %v (manifests %v)", s.Modules, s.Manifests) } } // Each layer is said; a check that could not run is an error on both, never a pass; a build seat from // before the layers is read as the gate. func TestAChecksLayersAreEachSaidAndAnErrorIsNeverAPass(t *testing.T) { asked := &link.CheckRequest{Owner: "novox", Repo: "mesh-catalog", Number: 3, Modules: []string{"gitea"}} c := checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Failed: "the facts snapshot cannot be read"}) if c.Verdict != "error" || c.Gate == nil || c.Gate.Verdict != "error" || c.RepoCheck == nil || c.RepoCheck.Verdict != "error" { t.Fatalf("a check that could not run said %+v", c) } if strings.Join(c.Gate.Modules, ",") != "gitea" { t.Errorf("the gate names %v", c.Gate.Modules) } c = checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Check: &link.CheckOutcome{Verdict: "warning", Summary: "wide"}}) if c.Gate == nil || c.Gate.Verdict != "warning" || c.RepoCheck != nil { t.Fatalf("an outcome without layers said %+v", c) } c = checkedOf(link.BuildResult{ID: "b", Ref: "abc", Checked: asked, Check: &link.CheckOutcome{Verdict: "pass", Gate: &link.CheckLayer{Verdict: "pass"}, RepoCheck: &link.CheckLayer{Verdict: "fail", Summary: "its merge-check.sh failed"}}}) if c.RepoCheck.Verdict != "fail" || c.Gate.Verdict != "pass" { t.Fatalf("the layers said %+v / %+v", c.Gate, c.RepoCheck) } } // **The check asks the planner, never a mapping of its own** (novox/hq ADR 0238): what a pull request // reaches is reachOfMerge's answer — the same that plans a merge — so a file at the root touches no // module in both, and what stands on a touched module is named as its dependents in both. func TestAPullRequestReachesWhatAMergeOfItWouldPlan(t *testing.T) { const catalogue = "http://forge.internal:20000/novox/mesh-catalog.git" nats := fromRepo("nats", catalogue, "modules/nats") nats.Source.BuiltFrom = "old" gitea := fromRepo("gitea", catalogue, "modules/gitea") gitea.Source.BuiltFrom = "old" tools := fromRepo("node-tools", "http://forge.internal:20000/novox/mesh-tools.git", "node-tools") entries := []inventory.Entry{nats, gitea, tools} // node-tools stands on the bus's image; a code edge, so a plan takes it along. edges := []inventory.Edge{{From: "node-tools", To: "nats", Kind: inventory.EdgeStandsOn}} read := map[string][]inventory.ReadRepository{} for _, c := range []struct { what string paths []string moved, dependents string width int unread string }{ {"a file at the root, read by no build", []string{"merge-check.sh"}, "", "", 0, "merge-check.sh"}, {"the bus's own directory", []string{"modules/nats/Dockerfile"}, "nats", "node-tools", 2, ""}, {"both, and a README", []string{"modules/gitea/index.ts", "modules/nats/x", "README.md"}, "gitea,nats", "node-tools", 3, "README.md"}, } { p := link.PullUpdated{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "head", Paths: c.paths} s := pullScope(p, entries, read, edges) m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "head", Paths: c.paths} r := reachOfMerge(m, entries, read, edges) if got := strings.Join(s.Modules, ","); got != c.moved || got != strings.Join(r.Moved(), ",") { t.Errorf("%s: the check reaches %q, the planner %v, wanted %q", c.what, got, r.Moved(), c.moved) } if got := strings.Join(s.Dependents, ","); got != c.dependents { t.Errorf("%s: dependents %q, wanted %q", c.what, got, c.dependents) } if s.Width != c.width || s.Width != len(r.Plan.Modules) { t.Errorf("%s: a merge would build %d, the planner says %d, wanted %d", c.what, s.Width, len(r.Plan.Modules), c.width) } if got := strings.Join(s.Unread, ","); got != c.unread { t.Errorf("%s: unread %q, wanted %q", c.what, got, c.unread) } } // A repository whose build another module's recipe packages: that module is reached through the // planner's `read`, and what stands on it after it. read["gitea"] = []inventory.ReadRepository{{Repository: "http://forge.internal:20000/novox/mesh-sdk.git"}} s := pullScope(link.PullUpdated{Owner: "novox", Repo: "mesh-sdk", Base: "main", Commit: "head", Paths: []string{"src/index.ts"}}, entries, read, edges) if strings.Join(s.Modules, ",") != "gitea" || len(s.Manifests) != 0 { t.Fatalf("a change to the source gitea packages reaches %v (manifests %v)", s.Modules, s.Manifests) } } // **Only a commit on the trunk is published** (novox/hq ADR 0238): a build of a commit off its repository's // default branch — a pull request's head, a branch built by hand, a rebuild or replay of one — is recorded // and never registered, so nothing can send it; a check or a dry run never is either. func TestABuildOffTheTrunkIsNeverPublished(t *testing.T) { on := link.BuildResult{ID: "b", Commit: "abc", Trunk: "main", OnTrunk: true} if err := publishable(on, ""); err != nil { t.Errorf("a build on the trunk was refused: %v", err) } off := link.BuildResult{ID: "b", Commit: "abc", Trunk: "main"} if err := publishable(off, ""); !errors.Is(err, errOffTheTrunk) || !strings.Contains(err.Error(), "main") { t.Errorf("a build off the trunk was let through: %v", err) } for _, r := range []link.BuildResult{ {ID: "c", Trunk: "main", OnTrunk: true, Check: &link.CheckOutcome{Verdict: "pass"}}, {ID: "d", Trunk: "main", OnTrunk: true, Checked: &link.CheckRequest{}}, {ID: "e", Trunk: "main", OnTrunk: true, DryRun: true}, } { if publishable(r, "") == nil { t.Errorf("%s, a check or a dry run, was publishable", r.ID) } } // A module that follows a branch other than the default: that branch is its trunk, and the default // is not. follows := link.BuildResult{ID: "g", Commit: "abc", Trunk: "master", Branches: []string{"nox-mesh"}} if err := publishable(follows, "nox-mesh"); err != nil { t.Errorf("a commit on the branch a module follows was refused: %v", err) } if err := publishable(link.BuildResult{ID: "h", Commit: "abc", Trunk: "master", OnTrunk: true, Branches: []string{"master"}}, "nox-mesh"); err == nil { t.Error("a commit on the default but not on the branch the module follows was published") } if err := publishable(link.BuildResult{ID: "i", Commit: "abc", Trunk: "main", Branches: []string{"feat/x"}}, ""); err == nil { t.Error("a feature branch's commit was published") } // A build seat older than the rule says nothing: let through and said, so the rule can reach the mesh. if err := publishable(link.BuildResult{ID: "f", Commit: "abc"}, ""); err != nil { t.Errorf("a build seat that said nothing was refused: %v", err) } } // **A verdict names the trunk commit it judged** (novox/hq ADR 0323 §3, issue 341): the controller's `checked` // carries the head and the trunk commit the build seat merged it into, the commits of the repositories read beside // it, how long it took, and the kept delivery plan records the trunk commit with its other inputs. A group's // composed check names, per member, the trunk commit that head was merged into (§6). func TestAVerdictNamesTheTrunkCommitItJudged(t *testing.T) { plan := &link.ChangePlan{Repository: "novox/mesh-host", Base: "main", Head: "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Summary: "builds mesh-host"} asked := &link.CheckRequest{Owner: "novox", Repo: "mesh-host", Number: 66, Base: "main", Plan: plan} c := checkedOf(link.BuildResult{ID: "b", Ref: "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Checked: asked, Check: &link.CheckOutcome{Verdict: "pass", Took: "4m12s", Head: "dfb1415aaaaa", Trunk: "d28ca95bbbbb", Gate: &link.CheckLayer{Verdict: "pass"}, RepoCheck: &link.CheckLayer{Verdict: "pass"}, Beside: map[string]string{"mesh-catalog": "0123456789ab", "mesh-lab": "ba9876543210"}}}) if c.Trunk != "d28ca95bbbbb" || c.Commit != "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" { t.Fatalf("the verdict names head %s and trunk %q", c.Commit, c.Trunk) } if c.Beside["mesh-catalog"] != "0123456789ab" || c.Beside["mesh-lab"] != "ba9876543210" { t.Fatalf("the verdict names the repositories read beside it as %v", c.Beside) } if c.Took != "4m12s" { t.Fatalf("the verdict says it took %q", c.Took) } if c.Plan == nil || c.Plan.Trunk != "d28ca95bbbbb" || c.Plan.Head != plan.Head { t.Fatalf("the kept plan records %+v", c.Plan) } if plan.Trunk != "" { t.Fatal("the plan the controller asked with was changed: the verdict's is a copy") } group := &link.CheckRequest{Owner: "novox", Repo: "mesh-controller", Number: 7, Base: "main", Group: "feat/x", Members: []link.GroupMember{{Owner: "novox", Repo: "mesh-host", Number: 8, Ref: "cccccccccccc0000", Base: "main"}}} c = checkedOf(link.BuildResult{ID: "g", Ref: "aaaaaaaaaaaa0000", Checked: group, Check: &link.CheckOutcome{ Verdict: "pass", Head: "aaaaaaaaaaaa", Trunk: "111111111111", Gate: &link.CheckLayer{Verdict: "pass"}, Members: []link.CheckedMember{{Owner: "novox", Repo: "mesh-host", Commit: "cccccccccccc", Trunk: "222222222222"}}}}) if len(c.Members) != 2 || c.Members[0].Trunk != "111111111111" || c.Members[1].Trunk != "222222222222" || c.Members[1].Commit != "cccccccccccc0000" { t.Fatalf("the composed check names its members %+v", c.Members) } } // **The merge made for a check is never registered** (novox/hq ADR 0323 §2): a build of it — a commit on no branch // of the forge — is refused, naming the trunk. The rule of ADR 0238 §1 stands unchanged; this holds it for the // merge commit a check makes. func TestABuildOfACheckMergeIsRefusedNamingTheTrunk(t *testing.T) { merge := link.BuildResult{ID: "b", Commit: "4b5c6d7e8f90aabbccddeeff0011223344556677", Trunk: "main"} err := publishable(merge, "") if !errors.Is(err, errOffTheTrunk) || !strings.Contains(err.Error(), "not on main") { t.Fatalf("a build of a check's merge was answered %v", err) } } // **A verdict no build judges names its trunk commit too** (novox/hq ADR 0323 §3): a change touching no module of a // repository with no merge-check.sh (novox/mesh-tools-go) is answered by the controller itself. Naming no trunk, the // merge tool refused it, asked again, got the same verdict, and refused for ever. func TestADirectVerdictNamesTheTrunkTheForgeAnnounced(t *testing.T) { p := link.PullUpdated{Owner: "novox", Repo: "mesh-tools-go", Number: 3, Base: "main", Commit: "1a2b3c4d5e6f7a8b9c0d", BaseSHA: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b"} c := directVerdict(p, link.ChangePlan{Repository: "novox/mesh-tools-go", Base: "main", Head: p.Commit}) if c.Trunk != "7a8b9c0d1e2f" || c.Plan == nil || c.Plan.Trunk != "7a8b9c0d1e2f" { t.Fatalf("the direct verdict names trunk %q, its plan %+v", c.Trunk, c.Plan) } if c.Verdict != "pass" || c.Commit != p.Commit { t.Fatalf("the direct verdict is %+v", c) } if c := directVerdict(link.PullUpdated{Owner: "novox", Repo: "x", Commit: "abc"}, link.ChangePlan{}); c.Trunk != "" { t.Fatalf("a forge that said no base head gave the trunk %q", c.Trunk) } } // A head checked again through mesh-delivery (`delivery check --members`, a recheck) reaches the same direct verdict: // it names the base head the forge last announced, kept on the member, or the merge tool would refuse it for ever. func TestARecheckedHeadsDirectVerdictNamesItsTrunk(t *testing.T) { m := orderMember{ID: "novox/mesh-tools-go@1a2b3c4d5e6f", Repository: "novox/mesh-tools-go", Base: "main", Head: "1a2b3c4d5e6f7a8b9c0d", Number: 3, BaseSHA: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b"} if c := directVerdict(m.pull(), link.ChangePlan{}); c.Trunk != "7a8b9c0d1e2f" { t.Fatalf("a rechecked head's direct verdict names trunk %q", c.Trunk) } }