mesh/delivery superseded: a newer delivery to the same trunk took over its walk
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
A change could delete or gut its merge-check.sh and skip its own tests, and a branch cut before the script escaped them. Where the base branch holds one, its script, declared parts and toolchain run against the change's tree, and the verdict says when the change lacks or alters the check (novox/hq issue 310).
211 lines
8.9 KiB
Go
211 lines
8.9 KiB
Go
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)
|
|
}
|
|
}
|