Skip to content
quarry Artifact [01adf697ee]
Login

Artifact [01adf697ee]

Artifact 01adf697ee6f98ec2c9f05837d13b82ef6816ad07b9a029f60c23936dcd3864f:


package oltp

import (
	"path/filepath"
	"testing"
	"time"
)

// runAt inserts a run and backdates its start, which AddCIRun always
// stamps with now.
func runAt(t *testing.T, db *DB, id, checkin, pair, status string, started time.Time) {
	t.Helper()
	if err := db.AddCIRun(CIRun{RunID: id, Repo: "alpha", Checkin: checkin,
		Pair: pair, Workflow: "w", Pipeline: "ci/p.json", Status: status}); err != nil {
		t.Fatal(err)
	}
	if _, err := db.db.Exec(`UPDATE ci_run SET started = ? WHERE run_id = ?`,
		started.UTC().Format(time.RFC3339), id); err != nil {
		t.Fatal(err)
	}
}

func runIDs(t *testing.T, db *DB) map[string]bool {
	t.Helper()
	rows, err := db.CIRuns("alpha", 100)
	if err != nil {
		t.Fatal(err)
	}
	out := map[string]bool{}
	for _, r := range rows {
		out[r.RunID] = true
	}
	return out
}

func openOLTP(t *testing.T) *DB {
	t.Helper()
	db, err := Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	return db
}

// What the window removes is the RETRIES. A check-in accumulates several
// runs from re-runs and supersessions, and that is where the unbounded
// growth was; the newest survives so the verdict badge can still name
// the run that produced it.
func TestPruningKeepsTheNewestRunPerCheckin(t *testing.T) {
	db := openOLTP(t)
	old := time.Now().Add(-200 * 24 * time.Hour)
	runAt(t, db, "r1", "c1", "", "failed", old)
	runAt(t, db, "r2", "c1", "", "failed", old.Add(time.Hour))
	runAt(t, db, "r3", "c1", "", "succeeded", old.Add(2*time.Hour))

	n, err := db.PruneCIRuns(time.Now().Add(-CIRunKeep))
	if err != nil {
		t.Fatal(err)
	}
	if n != 2 {
		t.Errorf("FAIL: retired %d runs, want 2", n)
	}
	left := runIDs(t, db)
	if !left["r3"] {
		t.Error("FAIL: the newest run for the check-in was pruned — the badge can no longer name it")
	}
	if left["r1"] || left["r2"] {
		t.Errorf("FAIL: a superseded retry survived: %v", left)
	}
}

// An unfinished run is never pruned, whatever its age. The row IS the
// poller's work list, and deleting it strands a run whose verdict
// nobody will ever record.
func TestAnUnfinishedRunIsNeverPruned(t *testing.T) {
	db := openOLTP(t)
	old := time.Now().Add(-400 * 24 * time.Hour)
	runAt(t, db, "done", "c1", "", "succeeded", old)
	runAt(t, db, "stuck", "c1", "", "running", old.Add(time.Minute))

	if _, err := db.PruneCIRuns(time.Now()); err != nil {
		t.Fatal(err)
	}
	if !runIDs(t, db)["stuck"] {
		t.Error("FAIL: an ancient unfinished run was pruned; it is a bug to find, not a row to hide")
	}
	// And it does not shield the finished ones behind it: "stuck" is not
	// a terminal run, so it is not the newest TERMINAL run either.
	if !runIDs(t, db)["done"] {
		t.Error("FAIL: the only finished run for the check-in went")
	}
}

// Nothing inside the window goes, however many retries it holds. The
// window is what makes this retention rather than a cap.
func TestRunsInsideTheWindowStay(t *testing.T) {
	db := openOLTP(t)
	recent := time.Now().Add(-2 * time.Hour)
	runAt(t, db, "a", "c1", "", "failed", recent)
	runAt(t, db, "b", "c1", "", "succeeded", recent.Add(time.Minute))

	n, err := db.PruneCIRuns(time.Now().Add(-CIRunKeep))
	if err != nil {
		t.Fatal(err)
	}
	if n != 0 {
		t.Errorf("FAIL: retired %d runs inside the window", n)
	}
}

// Subject is (repo, checkin, pair): a premerge run is about a PAIR of
// tips rather than one check-in (RFC 0009), so a check-in's own run and
// its premerge runs each keep their newest.
func TestAPremergeRunKeepsItsOwnNewest(t *testing.T) {
	db := openOLTP(t)
	old := time.Now().Add(-200 * 24 * time.Hour)
	runAt(t, db, "own1", "c1", "", "failed", old)
	runAt(t, db, "own2", "c1", "", "succeeded", old.Add(time.Hour))
	runAt(t, db, "pre1", "c1", "c1+t1", "failed", old)
	runAt(t, db, "pre2", "c1", "c1+t1", "succeeded", old.Add(time.Hour))

	if _, err := db.PruneCIRuns(time.Now().Add(-CIRunKeep)); err != nil {
		t.Fatal(err)
	}
	left := runIDs(t, db)
	for _, keep := range []string{"own2", "pre2"} {
		if !left[keep] {
			t.Errorf("FAIL: %s went; each subject keeps its own newest", keep)
		}
	}
	for _, gone := range []string{"own1", "pre1"} {
		if left[gone] {
			t.Errorf("FAIL: %s survived", gone)
		}
	}
}

// A run nobody superseded survives regardless of age: it is the newest
// for its check-in, which is the whole exception.
func TestALoneOldRunSurvives(t *testing.T) {
	db := openOLTP(t)
	runAt(t, db, "only", "c1", "", "succeeded", time.Now().Add(-3*365*24*time.Hour))
	if _, err := db.PruneCIRuns(time.Now()); err != nil {
		t.Fatal(err)
	}
	if !runIDs(t, db)["only"] {
		t.Error("FAIL: the only run for a check-in was pruned")
	}
}

// The poller's work list and the sweep read ONE status vocabulary. Two
// copies would drift, and the drift would either strand a run nobody
// polls or prune one somebody is waiting on.
func TestTheTerminalVocabularyIsShared(t *testing.T) {
	db := openOLTP(t)
	old := time.Now().Add(-200 * 24 * time.Hour)
	for i, st := range []string{"completed", "succeeded", "success", "failed",
		"cancelled", "canceled", "error"} {
		runAt(t, db, "t"+string(rune('a'+i)), "c1", "", st, old.Add(time.Duration(i)*time.Hour))
	}
	unfinished, err := db.UnfinishedCIRuns()
	if err != nil {
		t.Fatal(err)
	}
	if len(unfinished) != 0 {
		t.Errorf("FAIL: the poller sees %d of these as unfinished: %+v", len(unfinished), unfinished)
	}
	n, err := db.PruneCIRuns(time.Now())
	if err != nil {
		t.Fatal(err)
	}
	if n != 6 {
		t.Errorf("FAIL: retired %d of 7 terminal runs (6 plus the newest kept), want 6", n)
	}
}