run_test.go
⎇
Raw
1package ci
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "testing"
8
9 "hearthforge/internal/config"
10 "hearthforge/internal/db"
11)
12
13// newRunnerDB gives a Runner backed by a fresh database holding one repo.
14func newRunnerDB(t *testing.T) (*Runner, *db.DB) {
15 t.Helper()
16 dir := t.TempDir()
17 d, err := db.Open(filepath.Join(dir, "hearthforge.db"))
18 if err != nil {
19 t.Fatal(err)
20 }
21 t.Cleanup(func() { d.Close() })
22 for _, q := range []string{
23 `INSERT INTO repositories (id, name, is_private, created_at) VALUES (1, 'demo', 0, 'now')`,
24 `INSERT INTO users (id, username, created_at) VALUES (1, 'admin', 'now')`,
25 } {
26 if _, err := d.ExecContext(context.Background(), q); err != nil {
27 t.Fatal(err)
28 }
29 }
30 return New(&config.Config{DataDir: dir, CIMaxConcurrent: 1, CIMaxHistory: 50}, d), d
31}
32
33func insertRun(t *testing.T, d *db.DB, status string) int64 {
34 t.Helper()
35 res, err := d.ExecContext(context.Background(),
36 `INSERT INTO ci_runs (repo_id, trigger_source, commit_sha, status) VALUES (1, 'manual', 'abc', ?)`, status)
37 if err != nil {
38 t.Fatal(err)
39 }
40 id, _ := res.LastInsertId()
41 return id
42}
43
44func TestRetryRunRejectsUnfinishedRun(t *testing.T) {
45 r, d := newRunnerDB(t)
46 for _, status := range []string{"pending", "running", "queued"} {
47 id := insertRun(t, d, status)
48 if err := r.RetryRun(context.Background(), id, 1); !errors.Is(err, ErrRunNotFinished) {
49 t.Fatalf("status %s: got %v", status, err)
50 }
51 var got string
52 if err := d.QueryRowContext(context.Background(),
53 `SELECT status FROM ci_runs WHERE id = ?`, id).Scan(&got); err != nil {
54 t.Fatal(err)
55 }
56 if got != status {
57 t.Fatalf("status %s was changed to %s", status, got)
58 }
59 }
60}
61
62func TestRetryRunMissingRun(t *testing.T) {
63 r, _ := newRunnerDB(t)
64 if err := r.RetryRun(context.Background(), 999, 1); err == nil {
65 t.Fatal("expected an error")
66 }
67}
68
69// The terminal status list in RetryRun must match the statuses runs finish
70// with, otherwise no run could ever be retried.
71func TestRetryRunAcceptsFinishedStatuses(t *testing.T) {
72 r, d := newRunnerDB(t)
73 // A full concurrency slot keeps the retry in the queue, so the run does
74 // not start and this test never talks to a container engine.
75 r.running[-1] = &task{}
76 for _, status := range []string{"success", "failure", "warning", "cancelled", "skipped"} {
77 id := insertRun(t, d, status)
78 if err := r.RetryRun(context.Background(), id, 1); err != nil {
79 t.Fatalf("status %s: %v", status, err)
80 }
81 var got string
82 if err := d.QueryRowContext(context.Background(),
83 `SELECT status FROM ci_runs WHERE id = ?`, id).Scan(&got); err != nil {
84 t.Fatal(err)
85 }
86 if got != "queued" {
87 t.Fatalf("status %s became %s, want queued", status, got)
88 }
89 }
90}
91
92// finishSlot must not release a slot another execution owns: a retry that
93// restarted the same run holds its own task.
94func TestFinishSlotKeepsForeignTask(t *testing.T) {
95 r, _ := newRunnerDB(t)
96 mine, theirs := &task{}, &task{}
97 r.running[7] = theirs
98 r.finishSlot(7, mine)
99 if r.running[7] != theirs {
100 t.Fatal("finishSlot released a slot it does not own")
101 }
102 r.finishSlot(7, theirs)
103 if _, ok := r.running[7]; ok {
104 t.Fatal("finishSlot did not release its own slot")
105 }
106}
107