//go:build integration package builder import ( "os" "path/filepath" "strings" "testing" ) // **A delivery group's member is checked beside the other members' heads** (novox/hq ADR 0382, issue 867): a // mesh-host change and a mesh-controller change that need each other. The sibling's repository is read beside the // check at the sibling's head merged onto its base head, in place of the commit the mesh runs. // aSiblingWorld is the mesh's two core repositories as the trunk holds them, and a pull request into each that the // other needs: the controller's adds a lock line the node-engine's own check reads, and builds only with the // node-engine's new validator word. type aSiblingWorld struct { controller, host string // repositories controllerHead, hostHead string // the pull requests' whole heads controllerMain, hostMain string // the trunks' heads } func theSiblingWorld(t *testing.T) aSiblingWorld { t.Helper() var w aSiblingWorld // The controller's trunk: a judge saying who it is, its lock without the new line, and the check that judges it. controllerFiles := aJudgeOf("the trunk's controller") controllerFiles["lock.txt"] = "foundation: old\n" controllerFiles["module.json"] = `{"module":"mesh-controller"}` controllerFiles[CheckScript] = "true\n" var controllerRelease string w.controller, w.controllerMain, controllerRelease = aRepositoryWithARelease(t, controllerFiles, map[string]string{"r": "r"}) _ = controllerRelease gitInRepo(t, w.controller, "checkout", "--quiet", "-b", "change", "main") change := aJudgeOf("the sibling controller") change["lock.txt"] = "foundation: new\n" w.controllerHead = commitFiles(t, w.controller, change) gitInRepo(t, w.controller, "checkout", "--quiet", "main") // The node-engine's trunk: a validator, and the check that reads the controller's lock beside it. hostFiles := aHostSaying("the trunk's validator") hostFiles["module.json"] = `{"module":"mesh-host"}` hostFiles[CheckScript] = "set -e\ngrep -q 'foundation: new' \"$MESH_CHECK_BESIDE/mesh-controller/lock.txt\"\n" w.host, w.hostMain, _ = aRepositoryWithARelease(t, hostFiles, map[string]string{"r": "r"}) gitInRepo(t, w.host, "checkout", "--quiet", "-b", "change", "main") w.hostHead = commitFiles(t, w.host, aHostSaying("the sibling's validator")) gitInRepo(t, w.host, "checkout", "--quiet", "main") return w } func (w aSiblingWorld) beside() map[string]Beside { return map[string]Beside{"mesh-controller": {Repository: w.controller, Ref: w.controllerMain}, "mesh-host": {Repository: w.host, Ref: w.hostMain}} } // hostCheck is the node-engine's pull request checked, with or without its sibling. func (w aSiblingWorld) hostCheck(t *testing.T, id, workspace, cache string, siblings ...Sibling) CheckVerdict { t.Helper() return w.hostCheckAs(t, "", id, workspace, cache, siblings...) } // hostCheckAs is hostCheck for a request that names who judges: an older request says "validator" for a node-engine change. func (w aSiblingWorld) hostCheckAs(t *testing.T, judge, id, workspace, cache string, siblings ...Sibling) CheckVerdict { t.Helper() if workspace == "" { workspace = t.TempDir() } beside := w.beside() delete(beside, "mesh-host") v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: w.host, Ref: w.hostHead, Owner: "novox", Repo: "mesh-host", Number: 114, Base: "main", Judge: judge, JudgeCache: cache, Paths: []string{"validate/v.go"}, Modules: []string{"mesh-host"}, Beside: beside, Siblings: siblings, Toolchain: "golang"}, workspace, aFactsRegistry(t), GitCredential{}, nil) if err != nil { t.Fatal(err) } return v } // controllerCheck is the controller's pull request checked, with or without its sibling. func (w aSiblingWorld) controllerCheck(t *testing.T, id, cache string, siblings ...Sibling) CheckVerdict { t.Helper() beside := w.beside() delete(beside, "mesh-controller") v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: w.controller, Ref: w.controllerHead, Owner: "novox", Repo: "mesh-controller", Number: 346, Base: "main", Judge: "self", JudgeCache: cache, Paths: []string{"lock.txt"}, Modules: []string{"mesh-controller"}, Beside: beside, Siblings: siblings, Toolchain: "golang"}, t.TempDir(), aFactsRegistry(t), GitCredential{}, nil) if err != nil { t.Fatal(err) } return v } func (w aSiblingWorld) controllerSibling() Sibling { return Sibling{Repository: "novox/mesh-controller", Number: 346, Head: w.controllerHead, Base: "main", Dir: "mesh-controller"} } func (w aSiblingWorld) hostSibling() Sibling { return Sibling{Repository: "novox/mesh-host", Number: 114, Head: w.hostHead, Base: "main", Dir: "mesh-host"} } // The node-engine's change fails its own check without its sibling (the controller's lock beside it is the trunk's), // and passes with it read at its merge: the check reads the lock the sibling's change brings, the judge is the // controller built from the sibling's merge with the validator that controller vendors (mesh-sdk's), and the verdict names both. func TestAHostMemberIsCheckedBesideItsControllerSibling(t *testing.T) { aRuntimeThatBuildsTheJudge(t) w := theSiblingWorld(t) alone := w.hostCheck(t, "check-867-host-alone", "", "") if alone.Repo == nil || alone.Repo.Verdict != "fail" || len(alone.Siblings) != 0 { t.Fatalf("with no sibling the node-engine change should fail its own check (the lock beside it is the trunk's): %+v", alone.Repo) } v := w.hostCheck(t, "check-867-host", "", "", w.controllerSibling()) if v.Repo == nil || v.Repo.Verdict != "pass" { t.Fatalf("beside its sibling the node-engine change should pass its own check: %+v\n%s", v.Repo, v.Report) } if v.Gate == nil || v.Gate.Verdict != "pass" || !strings.Contains(v.Gate.Summary, "the sibling controller with the vendored validator") { t.Fatalf("the judge was not the controller built from the sibling's merge with the validator it vendors: %+v\n%s", v.Gate, v.Report) } baseCommit := w.controllerMain if len(v.Siblings) != 1 || v.Siblings[0] != (JudgedSibling{Repository: "novox/mesh-controller", Number: 346, Head: w.controllerHead, Base: "main", BaseCommit: baseCommit}) { t.Fatalf("the verdict names siblings %+v, not mesh-controller#346 at %s onto %s", v.Siblings, w.controllerHead, baseCommit) } if v.Beside["mesh-controller"] == baseCommit[:12] || v.Beside["mesh-controller"] == w.controllerHead[:12] { t.Fatalf("the controller was read at %s, which is the trunk's or the head and not their merge", v.Beside["mesh-controller"]) } if want := "sibling novox/mesh-controller#346 merge " + v.Beside["mesh-controller"]; v.JudgeFrom != want { t.Fatalf("the judge came from %q, not %q", v.JudgeFrom, want) } if want := "mesh-sdk's validator at the pin of sibling novox/mesh-controller#346 merge " + v.Beside["mesh-controller"] + " (a local mesh-sdk, v0.0.0, replaced)"; v.Validator != want { t.Fatalf("the validator was %q, not %q", v.Validator, want) } // The statuses' "beside siblings" part is the forge module's to write, from the verdict's siblings. if strings.Contains(v.Gate.Summary, "beside siblings") || strings.Contains(v.Repo.Summary, "beside siblings") { t.Fatalf("the check writes the siblings mark itself:\n%s\n%s", v.Gate.Summary, v.Repo.Summary) } // The base of the sibling is the trunk of the sibling's repository, as the trunk stood when the check cloned it. if v.Trunk != w.hostMain[:12] { t.Fatalf("the member's own trunk commit is %s, not %s", v.Trunk, w.hostMain[:12]) } } // The controller's change is judged by itself with the validator it vendors, whatever the node-engine sibling brings: the // node-engine holds no validator any more (issue 1391, novox/hq ADR 0446 clause 5). Nothing of the sibling is put into the judge. func TestAControllerMemberIsJudgedByItselfWithTheValidatorItVendors(t *testing.T) { aRuntimeThatBuildsTheJudge(t) w := theSiblingWorld(t) alone := w.controllerCheck(t, "check-867-controller-alone", "") v := w.controllerCheck(t, "check-867-controller", "", w.hostSibling()) for what, got := range map[string]CheckVerdict{"alone": alone, "beside its sibling": v} { if got.Gate == nil || got.Gate.Verdict != "pass" || !strings.Contains(got.Gate.Summary, "the sibling controller with the vendored validator") { t.Fatalf("the controller change %s was not judged by itself with the validator it vendors: %+v\n%s", what, got.Gate, got.Report) } } if len(v.Siblings) != 1 || v.Siblings[0].Repository != "novox/mesh-host" || v.Siblings[0].BaseCommit != w.hostMain { t.Fatalf("the verdict names siblings %+v", v.Siblings) } if v.Validator != "" { t.Fatalf("the validator was %q: a controller judged by itself has the one it vendors", v.Validator) } } // A node-engine member whose tree holds no validator (validate/ and internal/declaration/ deleted, as R4b deletes them) is judged // beside its controller sibling all the same: nothing of it is copied into the judge, so nothing is missing (issue 1391). func TestAHostMemberWithNoValidatorOfItsOwnDoesNotFailTheJudge(t *testing.T) { aRuntimeThatBuildsTheJudge(t) w := theSiblingWorld(t) gitInRepo(t, w.host, "checkout", "--quiet", "change") gitInRepo(t, w.host, "rm", "--quiet", "-r", "validate", "internal") gitInRepo(t, w.host, "commit", "--quiet", "-m", "the validator is mesh-sdk's") w.hostHead = gitInRepo(t, w.host, "rev-parse", "HEAD") gitInRepo(t, w.host, "checkout", "--quiet", "main") v := w.hostCheckAs(t, "validator", "check-1391-host-without-validator", "", "", w.controllerSibling()) if v.Gate == nil || v.Gate.Verdict != "pass" || !strings.Contains(v.Gate.Summary, "the sibling controller with the vendored validator") { t.Fatalf("a node-engine change with no validator of its own was not judged: %+v\n%s", v.Gate, v.Report) } alone := w.hostCheckAs(t, "validator", "check-1391-host-alone", "", "") if alone.Gate == nil || strings.Contains(alone.Gate.Summary, "could not be put in the judge") { t.Fatalf("the judge tried to copy the validator of a change that holds none: %+v", alone.Gate) } } // A sibling head that does not merge cleanly onto its base fails the check, naming the sibling and the files. func TestASiblingThatDoesNotMergeCleanlyFailsTheCheckNamingIt(t *testing.T) { aRuntimeThatBuildsTheJudge(t) w := theSiblingWorld(t) // The base moves under the sibling's head on the same line of the same file. gitInRepo(t, w.controller, "checkout", "--quiet", "main") commitFiles(t, w.controller, map[string]string{"lock.txt": "foundation: elsewhere\n"}) v := w.hostCheck(t, "check-867-conflict", "", "", w.controllerSibling()) if v.Verdict != "fail" || v.Gate == nil || v.Gate.Verdict != "fail" || v.Repo == nil || v.Repo.Verdict != "fail" { t.Fatalf("a sibling that does not merge was answered %+v / %+v", v.Gate, v.Repo) } for _, want := range []string{"novox/mesh-controller#346", "does not merge cleanly", "lock.txt", w.controllerHead[:12]} { if !strings.Contains(v.Summary, want) { t.Fatalf("the failure does not say %q: %s", want, v.Summary) } } if len(v.Siblings) != 1 || v.Siblings[0].Number != 346 { t.Fatalf("the verdict does not name the sibling: %+v", v.Siblings) } } // A check that could not learn its siblings reads none and says so; one with none to read is judged as before. func TestACheckThatCouldNotLearnItsSiblingsSaysSo(t *testing.T) { aRuntimeThatBuildsTheJudge(t) w := theSiblingWorld(t) beside := w.beside() delete(beside, "mesh-host") v, err := Check(t.Context(), Command, CheckSpec{ID: "check-867-unknown", Repository: w.host, Ref: w.hostHead, Owner: "novox", Repo: "mesh-host", Number: 114, Base: "main", Paths: []string{"validate/v.go"}, Modules: []string{"mesh-host"}, Beside: beside, SiblingsUnknown: "mesh-delivery did not answer", Toolchain: "golang"}, t.TempDir(), aFactsRegistry(t), GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.SiblingsUnknown != "mesh-delivery did not answer" || len(v.Siblings) != 0 || v.JudgeFrom != "" { t.Fatalf("the verdict says %q, siblings %+v, judge from %q", v.SiblingsUnknown, v.Siblings, v.JudgeFrom) } if !strings.Contains(v.Gate.Summary, "the trunk's controller with the vendored validator") { t.Fatalf("a check without siblings was not judged as before: %+v", v.Gate) } if strings.Contains(v.Gate.Summary, "[beside siblings") { t.Fatalf("the statuses name siblings that were not read: %s", v.Gate.Summary) } } // The judge a group made is kept only for the same inputs: the controller's merge and the base commits it was merged // onto. The same inputs take the judge kept; any other builds it again. func TestTheGroupsJudgeIsReusedOnlyForTheSameInputs(t *testing.T) { built := aRuntimeThatBuildsTheJudge(t) w := theSiblingWorld(t) cache := t.TempDir() w.hostCheck(t, "check-867-cache-1", "", cache, w.controllerSibling()) if n := builds(t, built); n != 1 { t.Fatalf("the first check built %d judge(s)", n) } v := w.hostCheck(t, "check-867-cache-2", "", cache, w.controllerSibling()) if n := builds(t, built); n != 1 || v.Gate.Verdict != "pass" { t.Fatalf("the same inputs built the judge again (%d builds) or did not pass: %+v", n, v.Gate) } // The controller sibling's head moves: another merge, another judge. gitInRepo(t, w.controller, "checkout", "--quiet", "change") w.controllerHead = commitFiles(t, w.controller, map[string]string{"extra.txt": "x"}) gitInRepo(t, w.controller, "checkout", "--quiet", "main") w.hostCheck(t, "check-867-cache-3", "", cache, w.controllerSibling()) if n := builds(t, built); n != 2 { t.Fatalf("a moved sibling head did not build the judge again: %d builds", n) } // The controller's base moves: the merge differs again. commitFiles(t, w.controller, map[string]string{"base.txt": "moved"}) w.hostCheck(t, "check-867-cache-4", "", cache, w.controllerSibling()) if n := builds(t, built); n != 3 { t.Fatalf("a moved base did not build the judge again: %d builds", n) } } // The contrast the isolation test stands on: built the old way, in the workspace with its HOME and no judge-build // environment, the same planted Go environment file takes effect in the group's judge build (its toolexec hook runs in // the judge's sources). Without this, the test below could not fail. func TestWithThePlainEnvironmentThePlantedWorkspaceReachesTheGroupsJudgeBuild(t *testing.T) { aRuntimeThatBuildsTheJudge(t) was := judgeBuildEnv judgeBuildEnv = func(string, string) []string { return nil } t.Cleanup(func() { judgeBuildEnv = was }) w := theSiblingWorld(t) workspace, ran := aPlantedWorkspace(t) w.hostCheck(t, "check-867-plain", workspace, "", w.controllerSibling()) if body, _ := readFile(ran); !strings.Contains(body, "/judge-src") { t.Fatalf("with the plain environment the planted hook did not run in the judge's build, so the isolation test proves nothing: %q", body) } } // **The group's judge is unreviewed code, built where nothing the change's script does can reach it**: a git hook, a // Go environment file and a Go cache planted in the workspace, and a script writing into its home, change nothing in the // judge; the judge cache is mounted into judge builds alone. func TestTheGroupsJudgeIsBuiltWhereNothingTheChangeDoesReachesIt(t *testing.T) { built := aRuntimeThatBuildsTheJudge(t) homes := filepath.Join(t.TempDir(), "homes") t.Setenv("JUDGE_HOMES", homes) runs := filepath.Join(t.TempDir(), "runs") t.Setenv("RUNS", runs) w := theSiblingWorld(t) // The member's own script (the base branch's) touches git in its tree and writes into its home. gitInRepo(t, w.host, "checkout", "--quiet", "main") commitFiles(t, w.host, map[string]string{CheckScript: "set -e\ngit status >/dev/null 2>&1 || true\n" + "echo planted > \"$HOME/.gitconfig\"\ngrep -q 'foundation: new' \"$MESH_CHECK_BESIDE/mesh-controller/lock.txt\"\n"}) w.hostMain = gitInRepo(t, w.host, "rev-parse", "main") gitInRepo(t, w.host, "checkout", "--quiet", "change") gitInRepo(t, w.host, "rebase", "--quiet", "main") w.hostHead = gitInRepo(t, w.host, "rev-parse", "HEAD") gitInRepo(t, w.host, "checkout", "--quiet", "main") workspace, ran := aPlantedWorkspace(t) cache := t.TempDir() v := w.hostCheck(t, "check-867-planted", workspace, cache, w.controllerSibling()) if v.Gate == nil || v.Gate.Verdict != "pass" || v.Repo == nil || v.Repo.Verdict != "pass" { t.Fatalf("the check did not pass: %+v %+v\n%s", v.Gate, v.Repo, v.Report) } if body, _ := readFile(ran); !strings.Contains(body, "/check/mesh-host") { t.Fatalf("the planted hook never ran in the member's tree, so this test proves nothing: %q", body) } if body, _ := readFile(ran); strings.Contains(body, "/judge-src") || judgeRanTheHook(t, ran) != "" { t.Fatalf("the hook planted in the workspace ran during the group's judge:\n%s", body) } if body, _ := readFile(homes); len(body) == 0 || strings.Contains(body, workspace) { t.Fatalf("the judge was built with a HOME in the workspace: %q", body) } said, _ := readFile(built) if !strings.Contains(said, "GOCACHE="+filepath.Join(cache, "go-cache")+" GOENV=off go build") { t.Fatalf("the judge was not built with the judge cache's Go cache and no Go environment file:\n%s", said) } // Only a judge build mounts the judge cache. body, _ := readFile(runs) for _, line := range strings.Split(strings.TrimSpace(body), "\n") { if !strings.Contains(line, " go build ") && strings.Contains(line, cache) { t.Fatalf("a container the check runs for the pull request names the judge cache: %s", line) } } // The sources the judge was built from are not left where the member's script could have written them. if _, err := os.Stat(filepath.Join(workspace, "check", "judge-src")); err == nil { t.Fatal("the judge's sources were left in the workspace") } } func readFile(path string) (string, error) { body, err := os.ReadFile(path) return string(body), err }