213 lines
8.9 KiB
Go
213 lines
8.9 KiB
Go
//go:build integration
|
|
|
|
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)
|
|
}
|
|
}
|