package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"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"
)
// A CLOSED LANDING'S MERGE RUNS ARE CANCELLED.
//
// Accepted or rejected, a landing is decided, and a verdict on its
// merge answers nobody. pikchr-studio's first deploy after a landing
// waited for a merge test started 40 seconds before the accept to run
// to its end, holding the runner the accept's own check-in needed.
func TestAClosedLandingsMergeRunsAreCancelled(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()
os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755)
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)
id, err := cli.LandingOpen(lease.Unguarded("test"), "dan", "feat", "trunk", "land feat", "")
if 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)
}
var mu sync.Mutex
var cancelled []string
runs := 0
labels := map[string]map[string]string{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
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 {
Labels map[string]string `json:"labels"`
}
json.NewDecoder(r.Body).Decode(&body)
runs++
labels[fmt.Sprintf("r-%d", runs)] = body.Labels
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{"run_id":"r-%d"}`, runs)
case r.URL.Path == "/runs":
w.Write([]byte(`[]`))
case strings.HasSuffix(r.URL.Path, "/cancel"):
cancelled = append(cancelled, strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/runs/"), "/cancel"))
case strings.HasPrefix(r.URL.Path, "/runs/"):
id := strings.TrimPrefix(r.URL.Path, "/runs/")
json.NewEncoder(w).Encode(ci.Run{RunID: id, Status: "running", Labels: labels[id]})
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
rg := newTestRegistrar(t, 25)
rg.engine = ci.New(srv.URL)
rg.deps = &api.Deps{Repos: api.NewRepos(map[string]*api.Repo{"r": {Index: ix, Zit: cli}})}
rg.landingSweep("r", ix) // seeds the open set, and starts the landing's merge run
mu.Lock()
started := runs
mu.Unlock()
if started != 1 {
t.Fatalf("FAIL: the open landing started %d merge runs, want 1", started)
}
rg.landingSweep("r", ix)
mu.Lock()
if len(cancelled) != 0 {
t.Fatalf("FAIL: an open landing's merge run was cancelled: %v", cancelled)
}
mu.Unlock()
zitIn(t, dir, "landing", "reject", id, "--user", "dan")
if _, err := pipe.Sync(); err != nil {
t.Fatal(err)
}
rg.landingSweep("r", ix)
mu.Lock()
defer mu.Unlock()
if len(cancelled) != 1 || cancelled[0] != "r-1" {
t.Fatalf("FAIL: after the landing closed, cancelled %v; want its merge run r-1", cancelled)
}
t.Log("ok: the merge run of an open landing kept, of a closed one cancelled")
}