package runner
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
)
// A TASK ON A BRANCH GETS THE PATHS THE BRANCH CHANGED; ONE ON TRUNK
// GETS NONE.
//
// The list lets a slow step test only what a branch touched. A
// premerge pair is the branch tip merged into its target, so it is
// asked about the tip. Trunk has no fork point, and a task there gets
// no list and runs whole: that is the backstop for what a branch
// reaches only through a dependency.
func TestATaskOnABranchGetsTheBranchsChangedPaths(t *testing.T) {
var asked []string
forge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h, ok := strings.CutSuffix(strings.TrimPrefix(r.URL.Path, "/api/v1/r/alpha/checkin/"), "/diff")
if !ok || r.URL.Query().Get("branch") != "true" {
http.NotFound(w, r)
return
}
asked = append(asked, h)
if h != "b1" {
http.Error(w, `{"error":"branch \"trunk\" has no fork point to diff from: not found"}`, http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(map[string]any{"from": "t0", "to": "b1", "files": []map[string]string{
{"status": "M", "name": "internal/web/timeline.go"}, {"status": "A", "name": "docs/x.md"},
}})
}))
defer forge.Close()
r, err := New(Config{
Bridge: "http://unused", Token: "t", Tasks: []string{"demo.changed"}, WorkDir: t.TempDir(),
Checkout: subdirCheckout{manifest: `{"tasks":{"demo.changed":["sh","-c","if [ -n \"${QUARRY_CHANGED:-}\" ]; then cat \"$QUARRY_CHANGED\"; else echo no-list; fi"]}}`},
Changed: ForgeBranch{API: forge.URL + "/api/v1/r/alpha"}.Changed,
})
if err != nil {
t.Fatal(err)
}
run := func(checkin string) string {
var logs, errs bytes.Buffer
if err := r.execute(context.Background(), "task", "demo.changed", checkin, "", "", nil, &logs, &errs); err != nil {
t.Fatalf("%s: %v: %s", checkin, err, errs.String())
}
return logs.String()
}
for _, checkin := range []string{"b1", "pair:b1:t1"} {
if out := run(checkin); !strings.Contains(out, "\ninternal/web/timeline.go\ndocs/x.md\n") {
t.Errorf("FAIL: %s: the task did not get the branch's paths: %q", checkin, out)
}
}
if out := run("t1"); !strings.Contains(out, "\nno-list\n") {
t.Errorf("FAIL: a trunk check-in got a list: %q", out)
}
if strings.Join(asked, ",") != "b1,b1,t1" {
t.Errorf("FAIL: the forge was asked about %v, want b1 (twice: the branch, then the pair's tip) and t1", asked)
}
}
// SCRIPTS/RACE.SH RUNS THE RACED PACKAGES A BRANCH CHANGED, AND ALL OF
// THEM WITH NO LIST OR A CHANGED BUILD INPUT.
//
// The go on PATH is a stand-in that prints its arguments, so the test
// reads which packages each list would race.
func TestTheRaceStepRunsWhatTheBranchChanged(t *testing.T) {
root := filepath.Join("..", "..")
script, err := filepath.Abs(filepath.Join(root, "scripts", "race.sh"))
if err != nil {
t.Fatal(err)
}
bin := t.TempDir()
if err := os.WriteFile(filepath.Join(bin, "go"), []byte("#!/bin/sh\necho \"go $*\"\n"), 0o755); err != nil {
t.Fatal(err)
}
// A tree with the raced packages, and a package nested in one.
tree := t.TempDir()
for _, d := range []string{"internal/children", "internal/xfer", "cmd/quarry", "internal/web", "internal/web/nested", "internal/index", "internal/api"} {
if err := os.MkdirAll(filepath.Join(tree, d), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tree, d, "x.go"), []byte("package x\n"), 0o644); err != nil {
t.Fatal(err)
}
}
if err := os.MkdirAll(filepath.Join(tree, "internal/web/assets"), 0o755); err != nil {
t.Fatal(err)
}
race := func(changed ...string) string {
cmd := exec.Command("sh", script)
cmd.Dir = tree
cmd.Env = append(os.Environ(), "PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"))
if changed != nil {
list := filepath.Join(t.TempDir(), "changed")
if err := os.WriteFile(list, []byte(strings.Join(changed, "\n")+"\n"), 0o644); err != nil {
t.Fatal(err)
}
cmd.Env = append(cmd.Env, "QUARRY_CHANGED="+list)
}
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("race.sh %v: %v\n%s", changed, err, out)
}
return string(out)
}
all := []string{"./internal/children/", "./internal/xfer/", "./cmd/quarry/", "./internal/web/", "./internal/index/"}
for _, c := range []struct {
name string
changed []string
want []string
}{
{"no list", nil, all},
{"a build input", []string{"go.mod", "internal/api/x.go"}, all},
{"web only", []string{"internal/web/timeline.go", "docs/api.md"}, []string{"./internal/web/"}},
{"a web asset", []string{"internal/web/assets/app.css"}, []string{"./internal/web/"}},
{"a package nested in web", []string{"internal/web/nested/x.go"}, nil},
{"index and xfer", []string{"internal/index/index.go", "internal/xfer/xfer.go"}, []string{"./internal/xfer/", "./internal/index/"}},
{"none raced", []string{"internal/api/x.go"}, nil},
} {
out := race(c.changed...)
var raced []string
for _, line := range strings.Split(out, "\n") {
if rest, ok := strings.CutPrefix(line, "go test -race "); ok {
for _, f := range strings.Fields(rest) {
if !strings.HasPrefix(f, ".") {
continue // a flag or a flag's value
}
if !slices.Contains(all, f) {
t.Errorf("FAIL: %s: go test was handed %q, not a raced package\n%s", c.name, f, out)
}
raced = append(raced, f)
}
}
}
for _, p := range all {
ran := slices.Contains(raced, p)
want := false
for _, w := range c.want {
want = want || w == p
}
if ran != want {
t.Errorf("FAIL: %s: %s raced=%v, want %v\n%s", c.name, p, ran, want, out)
}
}
if c.want == nil && !strings.Contains(out, "trunk runs them all") {
t.Errorf("FAIL: %s: racing nothing says nothing about why\n%s", c.name, out)
}
}
}