repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/scheduler/scheduler_test.go
Rawpackage scheduler
import (
"errors"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"bugabinga.net/luci/internal/history"
"bugabinga.net/luci/internal/inbox"
"bugabinga.net/luci/internal/trigger"
)
func TestPollPinsDefaultBranchAndDeduplicatesMinute(t *testing.T) {
root := t.TempDir()
bare, rev := createRepo(t, root, `job "nightly" {
image "alpine"
run "echo ok"
trigger {
schedule "0 3 * * *"
}
}
`)
_ = bare
minute := time.Date(2026, 3, 4, 3, 0, 0, 0, time.UTC)
cfg := Config{DataDir: filepath.Join(root, "data"), RepoRoots: []string{filepath.Join(root, "repos")}, InboxDir: filepath.Join(root, "data", "inbox"), Now: func() time.Time { return minute }}
if err := os.MkdirAll(cfg.InboxDir, 0o755); err != nil {
t.Fatal(err)
}
ids, err := Poll(cfg)
if err != nil || len(ids) != 1 {
t.Fatalf("ids=%v err=%v", ids, err)
}
pending, warnings, err := inbox.List(cfg.InboxDir)
if err != nil || len(warnings) != 0 || len(pending) != 1 {
t.Fatalf("pending=%#v warnings=%v err=%v", pending, warnings, err)
}
event, ok := pending[0].Event.(trigger.ScheduleEvent)
if !ok || event.Repo != "repo" || event.Job != "nightly" || event.Rev != rev || event.Ref != "refs/heads/main" || event.Minute != "2026-03-04T03:00Z" {
t.Fatalf("event=%#v", pending[0].Event)
}
ids, err = Poll(cfg)
if err != nil || len(ids) != 0 {
t.Fatalf("duplicate ids=%v err=%v", ids, err)
}
}
func TestPollThrottlesAndCoalescesFrequentSchedule(t *testing.T) {
root := t.TempDir()
createRepo(t, root, `job "frequent" {
image "alpine"
run "echo ok"
trigger {
schedule "* * * * *"
}
}
`)
minute := time.Date(2026, 3, 4, 3, 0, 0, 0, time.UTC)
cfg := Config{DataDir: filepath.Join(root, "data"), RepoRoots: []string{filepath.Join(root, "repos")}, InboxDir: filepath.Join(root, "data", "inbox"), Now: func() time.Time { return minute }}
if err := os.MkdirAll(cfg.InboxDir, 0o755); err != nil {
t.Fatal(err)
}
ids, err := Poll(cfg)
if err != nil || len(ids) != 1 {
t.Fatalf("first ids=%v err=%v", ids, err)
}
if err := os.Remove(filepath.Join(cfg.InboxDir, ids[0]+".json")); err != nil {
t.Fatal(err)
}
minute = minute.Add(14 * time.Minute)
ids, err = Poll(cfg)
if err != nil || len(ids) != 0 {
t.Fatalf("throttled ids=%v err=%v", ids, err)
}
minute = minute.Add(time.Minute)
ids, err = Poll(cfg)
if err != nil || len(ids) != 1 {
t.Fatalf("after interval ids=%v err=%v", ids, err)
}
minute = minute.Add(15 * time.Minute)
ids, err = Poll(cfg)
if err != nil || len(ids) != 0 {
t.Fatalf("coalesced ids=%v err=%v", ids, err)
}
}
func TestPollRecordsInvalidDefaultBranchConfigOncePerRevision(t *testing.T) {
root := t.TempDir()
createRepo(t, root, `job "broken" {
image "alpine"
run "echo ok"
trigger {
schedule "@daily"
}
}
`)
minute := time.Date(2026, 3, 4, 3, 0, 0, 0, time.UTC)
cfg := Config{DataDir: filepath.Join(root, "data"), RepoRoots: []string{filepath.Join(root, "repos")}, InboxDir: filepath.Join(root, "data", "inbox"), Now: func() time.Time { return minute }}
if err := os.MkdirAll(cfg.InboxDir, 0o755); err != nil {
t.Fatal(err)
}
for range 2 {
if _, err := Poll(cfg); err == nil {
t.Fatal("invalid config did not report error")
}
}
events, err := history.ReadAll(cfg.DataDir)
if err != nil || len(events) != 1 || events[0].Job != "schedule" || events[0].Status != "failed" {
t.Fatalf("events=%#v err=%v", events, err)
}
}
func TestUnscannableScheduleStateBlocksAdmission(t *testing.T) {
root := t.TempDir()
createRepo(t, root, `job "nightly" {
image "alpine"
run "echo ok"
trigger {
schedule "0 3 * * *"
}
}
`)
dataDir := filepath.Join(root, "data")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dataDir, "schedules"), []byte("not a directory"), 0o644); err != nil {
t.Fatal(err)
}
cfg := Config{DataDir: dataDir, RepoRoots: []string{filepath.Join(root, "repos")}, InboxDir: filepath.Join(dataDir, "inbox"), Now: func() time.Time {
return time.Date(2026, 3, 4, 3, 0, 0, 0, time.UTC)
}}
if err := os.MkdirAll(cfg.InboxDir, 0o755); err != nil {
t.Fatal(err)
}
ids, err := Poll(cfg)
if err == nil || len(ids) != 0 {
t.Fatalf("ids=%v err=%v", ids, err)
}
pending, _, listErr := inbox.List(cfg.InboxDir)
if listErr != nil || len(pending) != 0 {
t.Fatalf("pending=%#v err=%v", pending, listErr)
}
}
func TestUnreadableReservationStateBlocksNewAdmission(t *testing.T) {
root := t.TempDir()
createRepo(t, root, `job "frequent" {
image "alpine"
run "echo ok"
trigger {
schedule "* * * * *"
}
}
`)
minute := time.Date(2026, 3, 4, 3, 0, 0, 0, time.UTC)
cfg := Config{DataDir: filepath.Join(root, "data"), RepoRoots: []string{filepath.Join(root, "repos")}, InboxDir: filepath.Join(root, "data", "inbox"), Now: func() time.Time { return minute }}
if err := os.MkdirAll(cfg.InboxDir, 0o755); err != nil {
t.Fatal(err)
}
originalWrite := writeInbox
writeInbox = func(string, string, trigger.Event) error { return errors.New("blocked") }
t.Cleanup(func() { writeInbox = originalWrite })
if _, err := Poll(cfg); err == nil {
t.Fatal("publication failure not reported")
}
before, _, err := loadState(cfg.DataDir, "repo", "frequent")
if err != nil || before.Pending == nil {
t.Fatalf("before=%#v err=%v", before, err)
}
if err := os.WriteFile(filepath.Join(cfg.DataDir, "schedules", "000.json"), []byte("{"), 0o644); err != nil {
t.Fatal(err)
}
minute = minute.Add(15 * time.Minute)
if _, err := Poll(cfg); err == nil {
t.Fatal("malformed state did not block admission")
}
after, _, err := loadState(cfg.DataDir, "repo", "frequent")
if err != nil || after.PendingID != before.PendingID || after.Pending.Minute != before.Pending.Minute {
t.Fatalf("reservation changed: before=%#v after=%#v err=%v", before, after, err)
}
}
func TestPollDoesNotBackfillMissedMinute(t *testing.T) {
root := t.TempDir()
createRepo(t, root, `job "nightly" {
image "alpine"
run "echo ok"
trigger {
schedule "0 3 * * *"
}
}
`)
cfg := Config{DataDir: filepath.Join(root, "data"), RepoRoots: []string{filepath.Join(root, "repos")}, InboxDir: filepath.Join(root, "data", "inbox"), Now: func() time.Time {
return time.Date(2026, 3, 4, 3, 1, 0, 0, time.UTC)
}}
if err := os.MkdirAll(cfg.InboxDir, 0o755); err != nil {
t.Fatal(err)
}
ids, err := Poll(cfg)
if err != nil || len(ids) != 0 {
t.Fatalf("ids=%v err=%v", ids, err)
}
}
func createRepo(t *testing.T, root, config string) (string, string) {
t.Helper()
repos := filepath.Join(root, "repos")
bare := filepath.Join(repos, "repo.git")
work := filepath.Join(root, "work")
if err := os.MkdirAll(repos, 0o755); err != nil {
t.Fatal(err)
}
runGit(t, root, "init", "--bare", bare)
runGit(t, root, "init", "-b", "main", work)
runGit(t, work, "config", "user.email", "ci@example.invalid")
runGit(t, work, "config", "user.name", "CI")
if err := os.MkdirAll(filepath.Join(work, ".ci"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(work, ".ci", "ci.kdl"), []byte(config), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("repo\n"), 0o644); err != nil {
t.Fatal(err)
}
runGit(t, work, "add", ".")
runGit(t, work, "commit", "-m", "initial")
runGit(t, work, "remote", "add", "origin", bare)
runGit(t, work, "push", "origin", "main")
runGit(t, root, "--git-dir", bare, "symbolic-ref", "HEAD", "refs/heads/main")
rev := gitOutput(t, root, "--git-dir", bare, "rev-parse", "main")
return bare, rev
}
func runGit(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v: %s", args, err, out)
}
}
func gitOutput(t *testing.T, dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
return string(out[:len(out)-1])
}