T1.4: Create src/scheduler; move scheduler logic

Move scheduler.go and scheduler_test.go from src/core to the new
src/scheduler package. The scheduler still takes *[]domain.Job and
*core.Store (storage moves in T1.5). Update src/gui/app.go to import
the new package; rename the local variable to sched to avoid shadowing
the scheduler package name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-06-18 22:03:16 +03:00
parent c29d581fb1
commit ad0e45a7dd
4 changed files with 21 additions and 19 deletions
+110
View File
@@ -0,0 +1,110 @@
package scheduler
import (
"strings"
"testing"
"time"
"gitea.mixdep.ru/mix/gosentry/src/domain"
)
func TestNextRunTimeRejectsInvalidSchedules(t *testing.T) {
from := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC)
cases := []struct {
schedule string
desc string
}{
{"", "empty string"},
{" ", "whitespace only"},
{"@every", "bare @every without duration"},
{"@every xyz", "invalid @every duration string"},
{"@every -1s", "negative @every duration"},
{"@every 0s", "zero @every duration"},
{"not-a-cron", "invalid cron expression"},
{"60 * * * *", "cron minute out of range"},
}
for _, tc := range cases {
_, ok := nextRunTime(tc.schedule, from)
if ok {
t.Errorf("nextRunTime(%q) [%s]: expected false, got true", tc.schedule, tc.desc)
}
}
}
func TestPrepareNextRunSetsDisplayString(t *testing.T) {
jobs := []domain.Job{{Schedule: "*/5 * * * *", Enabled: true}}
s := &Scheduler{jobs: &jobs}
from := time.Date(2026, 6, 14, 12, 3, 0, 0, time.UTC)
s.prepareNextRun(&jobs[0], from)
want := "2026-06-14 12:05:00"
if jobs[0].NextRun != want {
t.Errorf("NextRun: got %q, want %q", jobs[0].NextRun, want)
}
wantDue := time.Date(2026, 6, 14, 12, 5, 0, 0, time.UTC)
if !jobs[0].NextDue.Equal(wantDue) {
t.Errorf("NextDue: got %v, want %v", jobs[0].NextDue, wantDue)
}
}
func TestPrepareNextRunSetsInvalidScheduleLabel(t *testing.T) {
jobs := []domain.Job{{Schedule: "not-a-cron", Enabled: true}}
s := &Scheduler{jobs: &jobs}
s.prepareNextRun(&jobs[0], time.Now())
if jobs[0].NextRun != "Invalid schedule" {
t.Errorf("NextRun: got %q, want 'Invalid schedule'", jobs[0].NextRun)
}
if !jobs[0].NextDue.IsZero() {
t.Errorf("NextDue should be zero for invalid schedule, got %v", jobs[0].NextDue)
}
}
func TestNextRunTimeSupportsEvery(t *testing.T) {
from := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC)
next, ok := nextRunTime("@every 10s", from)
if !ok {
t.Fatal("expected @every schedule to parse")
}
if want := from.Add(10 * time.Second); !next.Equal(want) {
t.Fatalf("expected %s, got %s", want, next)
}
}
func TestNextRunTimeSupportsCron(t *testing.T) {
from := time.Date(2026, 6, 14, 12, 3, 0, 0, time.UTC)
next, ok := nextRunTime("*/5 * * * *", from)
if !ok {
t.Fatal("expected cron schedule to parse")
}
want := time.Date(2026, 6, 14, 12, 5, 0, 0, time.UTC)
if !next.Equal(want) {
t.Fatalf("expected %s, got %s", want, next)
}
}
func TestRunningOutputIncludesInvocation(t *testing.T) {
started := time.Date(2026, 6, 17, 23, 40, 0, 0, time.Local)
job := domain.Job{
Name: "Backup",
Command: `C:\Program Files\FreeFileSync\FreeFileSync.exe`,
Arguments: `D:\Local\Jobs\Auto.ffs_batch`,
SuccessExitCodes: "0,1",
}
output := runningOutput(job, "Manual", started)
for _, want := range []string{
"Running since 2026-06-17 23:40:00",
"Manual",
job.Command,
job.Arguments,
"0,1",
"start_only",
} {
if !strings.Contains(output, want) {
t.Fatalf("expected running output to contain %q, got:\n%s", want, output)
}
}
}