Skip to content
quarry Artifact [891042bb54]
Login

Artifact [891042bb54]

Artifact 891042bb549ae1ce52fe3d2ed8ddc6fba92aa130f349a893f1c6619db2f843f4:


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) {
	// The runner is itself inside a CI task with a list of its own, as
	// it is when CI runs this test on a branch: a check-in with no list
	// must not get that one.
	stale := filepath.Join(t.TempDir(), "outer")
	if err := os.WriteFile(stale, []byte("outer/list.go\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	t.Setenv("QUARRY_CHANGED", stale)
	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
		// Not the environment this test runs in: CI runs it on a
		// branch, with a QUARRY_CHANGED of its own.
		var env []string
		for _, kv := range os.Environ() {
			if !strings.HasPrefix(kv, "QUARRY_CHANGED=") {
				env = append(env, kv)
			}
		}
		cmd.Env = append(env, "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)
		}
	}
}