feat: add durable schedule management and multi-node crash recovery
This commit is contained in:
107
pulse/schedule/rule_test.go
Normal file
107
pulse/schedule/rule_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func stamp(v string) time.Time {
|
||||
t, err := time.Parse(time.RFC3339Nano, v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
func calendar(t *testing.T, expr, zone, start string) *Calendar {
|
||||
t.Helper()
|
||||
c, err := Compile(Rule{Kind: "CRON", Cron: expr, Timezone: zone, StartAt: stamp(start)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
func TestCronCalendarUTCAndDST(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, expr, zone, from string
|
||||
want []string
|
||||
}{
|
||||
{"five_fields", "*/15 9-10 * * MON-FRI", "Asia/Shanghai", "2026-09-18T00:59:59Z", []string{"2026-09-18T01:00:00Z", "2026-09-18T01:15:00Z"}},
|
||||
{"spring_missing", "30 2 * * *", "America/New_York", "2026-03-08T00:00:00Z", []string{"2026-03-09T06:30:00Z"}},
|
||||
{"autumn_repeated", "30 1 * * *", "America/New_York", "2026-11-01T00:00:00Z", []string{"2026-11-01T05:30:00Z", "2026-11-01T06:30:00Z", "2026-11-02T06:30:00Z"}},
|
||||
{"half_hour_spring", "45 2 * * *", "Australia/Lord_Howe", "2026-10-03T14:59:00Z", []string{"2026-10-03T15:45:00Z", "2026-10-04T15:45:00Z"}},
|
||||
{"half_hour_autumn", "45 1 * * *", "Australia/Lord_Howe", "2026-04-04T13:00:00Z", []string{"2026-04-04T14:45:00Z", "2026-04-04T15:15:00Z"}},
|
||||
{"skipped_date", "0 0 * * *", "Pacific/Apia", "2011-12-29T10:00:00Z", []string{"2011-12-29T10:00:00Z", "2011-12-30T10:00:00Z"}},
|
||||
{"century_leap", "0 0 29 2 *", "UTC", "2096-03-01T00:00:00Z", []string{"2104-02-29T00:00:00Z", "2108-02-29T00:00:00Z"}},
|
||||
{"day_or_weekday", "0 0 13 * MON", "UTC", "2026-09-12T00:00:00Z", []string{"2026-09-13T00:00:00Z", "2026-09-14T00:00:00Z"}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := calendar(t, tc.expr, tc.zone, tc.from)
|
||||
at := stamp(tc.from)
|
||||
for _, want := range tc.want {
|
||||
got, err := c.AtOrAfter(at)
|
||||
if err != nil || got == nil || !got.Equal(stamp(want)) {
|
||||
t.Fatalf("at %s got %v %v want %s", at, got, err, want)
|
||||
}
|
||||
at = got.Add(time.Microsecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestRuleBoundsAndFixedRate(t *testing.T) {
|
||||
start := stamp("2026-09-17T00:00:00.000001Z")
|
||||
end := start.Add(3 * time.Second)
|
||||
c, err := Compile(Rule{Kind: "FIXED_RATE", Timezone: "UTC", StartAt: start, EndAt: &end, IntervalMS: 1000})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, at := range []time.Time{start.Add(-time.Hour), start, start.Add(time.Nanosecond), start.Add(1500 * time.Millisecond), end} {
|
||||
got, err := c.AtOrAfter(at)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if at.Equal(end) {
|
||||
if got != nil {
|
||||
t.Fatal("end is inclusive")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if got == nil || got.Before(at) || !got.Before(end) || (got.UnixMicro()-start.UnixMicro())%1000000 != 0 {
|
||||
t.Fatal("fixed rate drift")
|
||||
}
|
||||
}
|
||||
// 规则副本不能修改已编译日历。
|
||||
copy := c.Rule()
|
||||
*copy.EndAt = start
|
||||
if got, _ := c.AtOrAfter(start); got == nil {
|
||||
t.Fatal("rule alias")
|
||||
}
|
||||
c, err = Compile(Rule{Kind: "FIXED_RATE", Timezone: "UTC", StartAt: stamp("2000-01-01T00:00:00Z"), IntervalMS: 1000})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := c.AtOrAfter(stamp("9998-12-31T23:59:59.000001Z")); err != nil || got != nil {
|
||||
t.Fatal("calendar overflow")
|
||||
}
|
||||
}
|
||||
func TestRuleRejectsAmbiguityAndImpossibleDates(t *testing.T) {
|
||||
base := Rule{Kind: "CRON", Timezone: "UTC", StartAt: stamp("2026-01-01T00:00:00Z"), Cron: "* * * * *"}
|
||||
for _, expr := range []string{"@every 1s", "0 0 0 * * *", "TZ=UTC 0 0 * * *", "0 0 31 2 *", "60 * * * *", "0 0 * * 7", "*/0 * * * *"} {
|
||||
in := base
|
||||
in.Cron = expr
|
||||
if _, err := Compile(in); err == nil {
|
||||
t.Fatalf("accepted %q", expr)
|
||||
}
|
||||
}
|
||||
for _, zone := range []string{"", "Local", "../UTC", "Unknown/Zone"} {
|
||||
in := base
|
||||
in.Timezone = zone
|
||||
if _, err := Compile(in); err == nil {
|
||||
t.Fatalf("accepted zone %s", zone)
|
||||
}
|
||||
}
|
||||
in := base
|
||||
in.IntervalMS = 1000
|
||||
if _, err := Compile(in); err == nil {
|
||||
t.Fatal("accepted mixed rule")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user