Skip to content
quarry Artifact [9c91d77e53]
Login

Artifact [9c91d77e53]

Artifact 9c91d77e53ed5a23c553e30cccc994d588994be689a7d8b75cd05fbba584491c:


package web

import (
	"net/http/httptest"
	"regexp"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/ci"
	"craftdesign.group/quarry/internal/ops"
)

type attemptHook struct {
	attempt, outcome string
	open             bool
	body             string
}

var attemptOpenTag = regexp.MustCompile(`<details[^>]*data-hook="step-attempt"[^>]*>`)
var attemptAttrRe = regexp.MustCompile(`(data-attempt|data-outcome)="([^"]*)"`)

// attemptHooks reads every step-attempt block on the page, in order,
// with the markup up to the next block as its body.
func attemptHooks(body string) []attemptHook {
	locs := attemptOpenTag.FindAllStringIndex(body, -1)
	var out []attemptHook
	for i, loc := range locs {
		tag := body[loc[0]:loc[1]]
		h := attemptHook{open: regexp.MustCompile(`\sopen[\s>]`).MatchString(tag)}
		for _, m := range attemptAttrRe.FindAllStringSubmatch(tag, -1) {
			if m[1] == "data-attempt" {
				h.attempt = m[2]
			} else {
				h.outcome = m[2]
			}
		}
		end := len(body)
		if i+1 < len(locs) {
			end = locs[i+1][0]
		}
		h.body = body[loc[1]:end]
		out = append(out, h)
	}
	return out
}

func assertRetriedStep(t *testing.T, body string) {
	t.Helper()
	hooks := attemptHooks(body)
	if len(hooks) != 2 {
		t.Fatalf("step-attempt blocks = %d, want 2", len(hooks))
	}
	one, two := hooks[0], hooks[1]
	if one.attempt != "1" || one.outcome != "fail" || !one.open {
		t.Errorf("attempt 1 = %+v, want attempt=1 outcome=fail open", one)
	}
	if !strings.Contains(one.body, "--- FAIL: TestFlake") {
		t.Errorf("attempt 1 does not carry its failing output")
	}
	if two.attempt != "2" || two.outcome != "pass" || two.open {
		t.Errorf("attempt 2 = %+v, want attempt=2 outcome=pass closed", two)
	}
	if !strings.Contains(two.body, "--- PASS: TestFlake") {
		t.Errorf("attempt 2 does not carry its output")
	}
}

// #269: a step that failed its first attempt and passed its second
// shows both, from the drained record, the failure expanded.
func TestRunPageShowsEveryAttemptFromTheRecord(t *testing.T) {
	ix := landingIndex(t, false)
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		CI: fakeCI{runs: []api.CIRun{{RunID: "run-7", Checkin: "abc", Workflow: "w",
			Pipeline: "ci/test.json", Status: "completed", Started: "2026-08-29T01:00:00Z"}}}}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	eng := fakeEngine{run: ci.Run{RunID: "run-7", Status: "completed",
		Steps: map[string]ci.Step{"quarry.test": {Status: "completed", Attempts: 2, StartedAt: "2026-08-29T01:00:01Z"}}}}
	store := fakeLogStore{chunks: []ci.LogChunk{
		{Seq: 1, Stream: "out", Data: "--- FAIL: TestFlake", Step: "quarry.test", Attempt: 1},
		{Seq: 2, Stream: "marker", Data: "failed", Step: "quarry.test", Attempt: 1},
		{Seq: 1, Stream: "out", Data: "--- PASS: TestFlake", Step: "quarry.test", Attempt: 2},
		{Seq: 2, Stream: "marker", Data: "completed", Step: "quarry.test", Attempt: 2}}}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i8"}),
		WithEngine(eng), WithCILogs(store)}))
	t.Cleanup(srv.Close)
	assertRetriedStep(t, asDan(t, srv, "/r/alpha/ci/run/run-7"))
}

// The same from a run the engine still holds: each attempt is read
// from its own lane, not only the latest.
func TestRunPageShowsEveryAttemptFromTheEngine(t *testing.T) {
	eng := fakeEngine{
		run: ci.Run{RunID: "run-8", Status: "completed",
			Steps: map[string]ci.Step{"quarry.test": {Status: "completed", Attempts: 2, StartedAt: "2026-08-29T01:00:01Z"}}},
		attempts: map[string]map[int][]ci.LogChunk{"quarry.test": {
			1: {{Seq: 1, Stream: "out", Data: "--- FAIL: TestFlake"}, {Seq: 2, Stream: "marker", Data: "failed"}},
			2: {{Seq: 1, Stream: "out", Data: "--- PASS: TestFlake"}, {Seq: 2, Stream: "marker", Data: "completed"}},
		}}}
	srv := runPageServer(t, eng, []api.CIRun{{RunID: "run-8", Checkin: "abc", Workflow: "w", Pipeline: "ci/test.json", Status: "completed", Started: "2026-08-29T01:00:00Z"}})
	assertRetriedStep(t, asDan(t, srv, "/r/alpha/ci/run/run-8"))
}

// A retried-then-green step is not the step that broke a red run: the
// checks card and the run list name the step whose LAST attempt failed.
func TestFailTailJudgesAStepByItsLastAttempt(t *testing.T) {
	store := fakeLogStore{chunks: []ci.LogChunk{
		{Seq: 1, Stream: "out", Data: "--- FAIL: TestLint", Step: "quarry.fmt", Attempt: 1},
		{Seq: 2, Stream: "marker", Data: "failed", Step: "quarry.fmt", Attempt: 1},
		{Seq: 1, Stream: "out", Data: "--- FAIL: TestFlake", Step: "quarry.test", Attempt: 1},
		{Seq: 2, Stream: "marker", Data: "failed", Step: "quarry.test", Attempt: 1},
		{Seq: 1, Stream: "out", Data: "ok", Step: "quarry.test", Attempt: 2},
		{Seq: 2, Stream: "marker", Data: "completed", Step: "quarry.test", Attempt: 2}}}
	if step, _, _ := failTail(nil, store, "alpha", "run-1"); step != "quarry.fmt" {
		t.Errorf("failTail step = %q, want quarry.fmt", step)
	}
	if step, _ := failSummary(store, "alpha", "run-1"); step != "quarry.fmt" {
		t.Errorf("failSummary step = %q, want quarry.fmt", step)
	}
}