package builder import ( "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) // novox/hq issue 310: the build seat ran the merge-check.sh found in the pull request's head, so a change // could delete or gut it and skip its own tests, and a branch cut before the script escaped them. The base // branch's script — its parts and its toolchain with it — judges the change's tree. // aPullRequest is a repository whose main holds base, and a branch "change" off it that holds change on // top (a nil body deletes the file): cloned as the check clones it, at the change's head. It answers the // clone's tree. func aPullRequest(t *testing.T, base, change map[string]*string) string { t.Helper() origin := t.TempDir() git := func(dir string, args ...string) string { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org") out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } return strings.TrimSpace(string(out)) } write := func(files map[string]*string) { for name, body := range files { at := filepath.Join(origin, name) if body == nil { if err := os.Remove(at); err != nil { t.Fatal(err) } continue } if err := os.MkdirAll(filepath.Dir(at), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(at, []byte(*body), 0o755); err != nil { t.Fatal(err) } } git(origin, "add", "-A") git(origin, "commit", "--quiet", "--allow-empty", "-m", "x") } git(origin, "init", "--quiet", "-b", "main") write(base) git(origin, "checkout", "--quiet", "-b", "change") write(change) head := git(origin, "rev-parse", "HEAD") git(origin, "checkout", "--quiet", "main") root := t.TempDir() git(root, "clone", "--quiet", origin, "checked") tree := filepath.Join(root, "checked") git(tree, "checkout", "--quiet", head) return tree } func s(body string) *string { return &body } // judged runs the check that judges the change as the build seat would — each part a plain sh here. func judged(t *testing.T, tree, base string) (Judging, *Layer) { t.Helper() j, err := TheCheckThatJudges(t.Context(), tree, base) if err != nil { t.Fatal(err) } if j.Script == nil { return j, nil } if err := j.Put(tree); err != nil { t.Fatal(err) } var out tail layer := ownCheck(t.Context(), CheckSpec{Toolchain: "go-image", Toolchains: map[string]string{"go": "go-image", "typescript": "ts-image"}}, ScriptParts(j.Script), tree, &out, func() bool { return false }, func(image, script string) *exec.Cmd { cmd := exec.CommandContext(t.Context(), "sh", script) cmd.Dir = tree return cmd }, func(string, string, ...any) {}) return j, layer } // main's check: the suite fails unless the tree holds what main requires. const mainsCheck = "#!/bin/sh\nset -eu\ntest -f covered || { echo 'FAIL: the suite does not pass'; exit 1; }\n" func TestAHeadWithoutTheScriptIsJudgedByMains(t *testing.T) { // Deleted by the change — or never there, on a branch cut before main had it: the head lacks it. tree := aPullRequest(t, map[string]*string{CheckScript: s(mainsCheck)}, map[string]*string{CheckScript: nil}) j, layer := judged(t, tree, "main") if layer == nil || layer.Verdict != "fail" || !strings.Contains(layer.Summary, "does not pass") { t.Fatalf("a head without the script answered %+v — main's script was not what judged it", layer) } if !strings.Contains(j.Said, "the change holds no "+CheckScript) || !strings.Contains(j.Said, "main's judged it") { t.Errorf("the verdict does not say main's judged it: %q", j.Said) } // And a head that holds what main's script asks passes by it. tree = aPullRequest(t, map[string]*string{CheckScript: s(mainsCheck)}, map[string]*string{CheckScript: nil, "covered": s("x")}) if _, layer = judged(t, tree, "main"); layer == nil || layer.Verdict != "pass" { t.Fatalf("a head main's script passes answered %+v", layer) } } func TestAHeadThatGutsTheScriptIsStillJudgedByMains(t *testing.T) { tree := aPullRequest(t, map[string]*string{CheckScript: s(mainsCheck)}, map[string]*string{CheckScript: s("#!/bin/sh\nexit 0\n")}) j, layer := judged(t, tree, "main") if layer == nil || layer.Verdict != "fail" { t.Fatalf("a gutted script answered %+v — the change's own copy judged it", layer) } if !strings.Contains(j.Said, "ALTERS ITS OWN CHECK") || !strings.Contains(j.Said, CheckScript) { t.Errorf("the verdict does not say the change alters its check: %q", j.Said) } // Gutting the declared part instead, or declaring none: main's lines and main's part still judge. two := "#!/bin/sh\n# mesh-check-toolchain: typescript\n# mesh-check-also: go replays/merge-check.sh\nexit 0\n" tree = aPullRequest(t, map[string]*string{CheckScript: s(two), "replays/merge-check.sh": s(mainsCheck)}, map[string]*string{CheckScript: s("#!/bin/sh\n# mesh-check-toolchain: typescript\nexit 0\n"), "replays/merge-check.sh": s("exit 0\n")}) j, layer = judged(t, tree, "main") if layer == nil || layer.Verdict != "fail" || !strings.Contains(layer.Summary, "replays/merge-check.sh failed") { t.Fatalf("a gutted part answered %+v", layer) } if parts := ScriptParts(j.Script); len(parts) != 2 || parts[0].Toolchain != "typescript" { t.Errorf("main's toolchain and parts did not judge: %+v", parts) } if !strings.Contains(j.Said, CheckScript+", replays/merge-check.sh") { t.Errorf("the verdict does not name both altered scripts: %q", j.Said) } } // A change that legitimately alters the check is judged by main's, says so, and passes when main's passes: // its own version judges the pull requests after it. func TestAChangeToTheCheckIsJudgedByTheOneItReplaces(t *testing.T) { tree := aPullRequest(t, map[string]*string{CheckScript: s(mainsCheck), "covered": s("x")}, map[string]*string{CheckScript: s(mainsCheck + "test -f also-covered\n")}) j, layer := judged(t, tree, "main") if layer == nil || layer.Verdict != "pass" || !strings.Contains(j.Said, "ALTERS ITS OWN CHECK") { t.Fatalf("a change to the check answered %+v, said %q", layer, j.Said) } // The same script as main's: nothing to say. tree = aPullRequest(t, map[string]*string{CheckScript: s(mainsCheck), "covered": s("x")}, map[string]*string{"other.go": s("package x\n")}) if j, layer = judged(t, tree, "main"); layer == nil || layer.Verdict != "pass" || j.Said != "" { t.Fatalf("an unchanged check answered %+v, said %q", layer, j.Said) } } // Where main holds no script, what was so stays: the head's, or none. func TestWhereMainHoldsNoScriptTheHeadsRuns(t *testing.T) { tree := aPullRequest(t, map[string]*string{"README.md": s("x")}, map[string]*string{CheckScript: s("echo 'the first check'; exit 0\n")}) j, layer := judged(t, tree, "main") if layer == nil || layer.Verdict != "pass" || j.Said != "" { t.Fatalf("the head's first script answered %+v, said %q", layer, j.Said) } tree = aPullRequest(t, map[string]*string{"README.md": s("x")}, map[string]*string{"b.md": s("y")}) if j, _ = judged(t, tree, "main"); j.Script != nil { t.Fatalf("neither holds a script, and one judged: %q", j.Script) } // No base asked (a check by hand without one): the head's, as before. tree = aPullRequest(t, map[string]*string{CheckScript: s(mainsCheck)}, map[string]*string{CheckScript: s("exit 0\n")}) if j, layer = judged(t, tree, ""); layer == nil || layer.Verdict != "pass" { t.Fatalf("with no base the head's answered %+v", layer) } } // A base branch that cannot be read is an error — never a fall back to the change's own script. func TestABaseThatCannotBeReadIsAnError(t *testing.T) { tree := aPullRequest(t, map[string]*string{CheckScript: s(mainsCheck)}, map[string]*string{CheckScript: s("exit 0\n")}) if _, err := TheCheckThatJudges(t.Context(), tree, "no-such-branch"); err == nil || !strings.Contains(err.Error(), "no-such-branch") { t.Fatalf("an unreadable base said %v", err) } } // The whole check, against a real container runtime: a head without the script fails by main's. func TestACheckOfAHeadWithoutTheScriptRunsMains(t *testing.T) { registry := checkEnvironment(t) tree := aPullRequest(t, map[string]*string{CheckScript: s(mainsCheck)}, map[string]*string{CheckScript: nil}) head, err := gitIn(t.Context(), tree, "rev-parse", "HEAD") if err != nil { t.Fatal(err) } origin, err := gitIn(t.Context(), tree, "remote", "get-url", "origin") if err != nil { t.Fatal(err) } v, err := Check(t.Context(), Command, CheckSpec{ID: fmt.Sprintf("check-base-%d", time.Now().UnixNano()), Repository: strings.TrimSpace(string(origin)), Ref: strings.TrimSpace(string(head)), Owner: "novox", Repo: "hq", Base: "main", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.Repo == nil || v.Repo.Verdict != "fail" || !strings.Contains(v.Repo.Summary, "main's judged it") { t.Fatalf("a head without the script answered %+v\n%s", v.Repo, v.Report) } }