package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"sync"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/ci"
"craftdesign.group/quarry/internal/events"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ingest"
"craftdesign.group/quarry/internal/lease"
"craftdesign.group/quarry/internal/zitcli"
)
// subjectEngine is a CI engine that records the subject of every run
// it is asked to start.
func subjectEngine(t *testing.T) (*ci.Engine, func() []string) {
t.Helper()
var mu sync.Mutex
var started []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/workflows":
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"status":"registered"}`))
case r.URL.Path == "/runs" && r.Method == http.MethodPost:
var body struct {
Input map[string]any `json:"input"`
}
json.NewDecoder(r.Body).Decode(&body)
mu.Lock()
started = append(started, fmt.Sprint(body.Input["checkin"]))
id := fmt.Sprintf("r-%d", len(started))
mu.Unlock()
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{"run_id":%q}`, id)
case r.URL.Path == "/runs":
w.Write([]byte(`[]`))
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
return ci.New(srv.URL), func() []string {
mu.Lock()
defer mu.Unlock()
out := started
started = nil
return out
}
}
// A LANDING IS TESTED AS ITS MERGE, AS A PULL REQUEST IS.
//
// quarry#290: the sweep after a fold starts a pair run for a landing
// whose branch tip has none, and nothing again for that tip. A target
// that moves re-runs nothing, as on a pull request; a push to the
// branch tests its merge with the target as it is then.
func TestTheSweepTestsALandingAsItsMerge(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
write := func(name, body string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
zitIn(t, dir, "init", ".")
write("ci/test.json", `{"name":"test","version":"1.0","steps":[]}`)
write("f.txt", "root\n")
zitIn(t, dir, "add", "ci/test.json", "f.txt")
zitIn(t, dir, "commit", "-m", "root", "--user", "u")
write("g.txt", "feat\n")
zitIn(t, dir, "add", "g.txt")
zitIn(t, dir, "commit", "-m", "feat", "--branch", "feat", "--user", "u")
cli := zitcli.New(dir)
if _, err := cli.LandingOpen(lease.Unguarded("test"), "dan", "feat", "trunk", "land feat", ""); err != nil {
t.Fatal(err)
}
ev, err := events.Start(filepath.Join(t.TempDir(), "nats"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(ev.Close)
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
pipe := ingest.NewPipeline(cli, ev, ix, "r", "n1")
if _, err := pipe.Sync(); err != nil {
t.Fatal(err)
}
rg := newTestRegistrar(t, 25)
engine, started := subjectEngine(t)
rg.engine = engine
rg.deps = &api.Deps{Repos: api.NewRepos(map[string]*api.Repo{"r": {Index: ix, Zit: cli}})}
tip := func(b string) string {
t.Helper()
h, err := ix.BranchTip(b)
if err != nil || h == "" {
t.Fatalf("no tip for %s: %v", b, err)
}
return h
}
sweep := func() []string {
t.Helper()
open, err := ix.Landings("open", 500)
if err != nil {
t.Fatal(err)
}
rg.pairSweep("r", ix, open, nil)
return started()
}
feat, root := tip("feat"), tip("trunk")
if got := sweep(); len(got) != 1 || got[0] != ci.Pair(feat, root) {
t.Fatalf("FAIL: an opened merge-mode landing started %v, want its pair with trunk", got)
}
if got := sweep(); len(got) != 0 {
t.Fatalf("FAIL: a pair already tested against trunk as it is started again: %v", got)
}
zitIn(t, dir, "update", "trunk")
write("f.txt", "moved\n")
zitIn(t, dir, "commit", "-m", "trunk moves", "--user", "u")
if _, err := pipe.Sync(); err != nil {
t.Fatal(err)
}
moved := tip("trunk")
if got := sweep(); len(got) != 0 {
t.Fatalf("FAIL: trunk moving re-ran %v; a pull request's checks do not re-run for its base", got)
}
zitIn(t, dir, "update", "feat")
write("g.txt", "feat, again\n")
zitIn(t, dir, "commit", "-m", "feat again", "--user", "u")
if _, err := pipe.Sync(); err != nil {
t.Fatal(err)
}
pushed := tip("feat")
if got := sweep(); len(got) != 1 || got[0] != ci.Pair(pushed, moved) {
t.Fatalf("FAIL: a push to the branch started %v, want its merge with trunk at %.12s", got, moved)
}
t.Log("ok: a merge run when the landing opens and when its branch moves, none when trunk moves")
}