internal/ci/sched_test.go
138 lines · 4664 bytes
1 symbol in this file
1package ci
2
3import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "gitbay.org/gitbay/internal/store"
11)
12
13// TestSchedulerRunDue drives one scheduler pass against a real store and
14// bare repo: a due entry queues a build, sets a pending status, and
15// advances next_run; entries whose job vanished are dropped.
16func TestSchedulerRunDue(t *testing.T) {
17 st, err := store.Open(":memory:")
18 if err != nil {
19 t.Fatal(err)
20 }
21 defer st.Close()
22 if err := st.MigrateUp(); err != nil {
23 t.Fatal(err)
24 }
25 uid, err := st.CreateUser("alice", false)
26 if err != nil {
27 t.Fatal(err)
28 }
29 _ = uid
30 repoID, err := st.CreateRepo("user", uid, "app", "public")
31 if err != nil {
32 t.Fatal(err)
33 }
34
35 // A bare repo whose main holds a ci.yml with one scheduled job.
36 work := t.TempDir()
37 env := append(os.Environ(),
38 "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null",
39 "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
40 "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test")
41 git := func(dir string, args ...string) {
42 t.Helper()
43 cmd := exec.Command("git", args...)
44 cmd.Dir = dir
45 cmd.Env = env
46 if out, err := cmd.CombinedOutput(); err != nil {
47 t.Fatalf("git %v: %v\n%s", args, err, out)
48 }
49 }
50 src := filepath.Join(work, "src")
51 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
52 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"),
53 []byte("jobs:\n nightly:\n schedule: \"0 6 * * *\"\n steps: [echo hi]\n"), 0o644)
54 git(work, "init", "-q", "-b", "main", "src")
55 git(src, "add", ".")
56 git(src, "commit", "-q", "-m", "base")
57 bare := filepath.Join(work, "bare.git")
58 git(work, "clone", "-q", "--bare", src, "bare.git")
59
60 now := time.Now()
61 past := now.Add(-time.Hour).UTC().Format("2006-01-02T15:04:05Z")
62 if err := st.SyncSchedules(repoID, []store.Schedule{
63 {RepoID: repoID, Job: "nightly", Cron: "0 6 * * *", NextRun: past},
64 {RepoID: repoID, Job: "gone", Cron: "0 7 * * *", NextRun: past},
65 }); err != nil {
66 t.Fatal(err)
67 }
68
69 s := &Scheduler{St: st, SiteURL: "https://x.test",
70 RepoDir: func(owner, name string) string { return bare }}
71 s.RunDue(now)
72
73 builds, err := st.ListBuilds(repoID, store.BuildFilter{}, 10)
74 if err != nil || len(builds) != 1 {
75 t.Fatalf("builds after run: %v %v", builds, err)
76 }
77 if builds[0].Job != "nightly" || builds[0].Status != "pending" || builds[0].Ref != "main" {
78 t.Fatalf("queued build wrong: %+v", builds[0])
79 }
80 statuses, _ := st.ListCommitStatuses(repoID, builds[0].SHA)
81 if len(statuses) != 1 || statuses[0].Context != "ci/nightly" || statuses[0].State != "pending" {
82 t.Fatalf("status wrong: %+v", statuses)
83 }
84 // next_run advanced past now; the vanished job's entry is gone.
85 due, _ := st.DueSchedules(now.UTC().Format("2006-01-02T15:04:05Z"))
86 if len(due) != 0 {
87 t.Fatalf("still due after run: %+v", due)
88 }
89 all, _ := st.DueSchedules("9999-01-01T00:00:00Z")
90 if len(all) != 1 || all[0].Job != "nightly" {
91 t.Fatalf("schedule set after run: %+v", all)
92 }
93 // Cron fires in server-local time, stored as UTC.
94 cr, _ := ParseCron("0 6 * * *")
95 if want := cr.Next(now).UTC().Format("2006-01-02T15:04:05Z"); all[0].NextRun != want {
96 t.Fatalf("next_run = %s, want %s", all[0].NextRun, want)
97 }
98
99 // A second pass fires nothing: next_run is in the future.
100 s.RunDue(now)
101 if builds, _ = st.ListBuilds(repoID, store.BuildFilter{}, 10); len(builds) != 1 {
102 t.Fatalf("second pass queued extra builds: %+v", builds)
103 }
104
105 // Due again while the first build is still pending: nothing is
106 // queued, so a repository no runner serves holds one row per
107 // scheduled job, not one per tick (#206). Once that build finishes,
108 // the next tick queues again — a schedule re-runs an unchanged
109 // commit on purpose.
110 if err := st.SetScheduleNext(repoID, "nightly", past); err != nil {
111 t.Fatal(err)
112 }
113 s.RunDue(now)
114 if builds, _ = st.ListBuilds(repoID, store.BuildFilter{}, 10); len(builds) != 1 {
115 t.Fatalf("tick with the last build pending queued another: %+v", builds)
116 }
117 b, ok, err := st.ClaimBuild(nil, false)
118 if err != nil || !ok {
119 t.Fatalf("claim: %v ok=%v", err, ok)
120 }
121 if err := st.SetScheduleNext(repoID, "nightly", past); err != nil {
122 t.Fatal(err)
123 }
124 s.RunDue(now)
125 if builds, _ = st.ListBuilds(repoID, store.BuildFilter{}, 10); len(builds) != 1 {
126 t.Fatalf("tick with the last build running queued another: %+v", builds)
127 }
128 if err := st.FinishBuild(b.ID, "success"); err != nil {
129 t.Fatal(err)
130 }
131 if err := st.SetScheduleNext(repoID, "nightly", past); err != nil {
132 t.Fatal(err)
133 }
134 s.RunDue(now)
135 if builds, _ = st.ListBuilds(repoID, store.BuildFilter{}, 10); len(builds) != 2 {
136 t.Fatalf("tick after the last build finished did not queue: %+v", builds)
137 }
138}