Files
xlgo-core/cron/cron_c12_test.go
2026-07-14 10:24:10 +08:00

322 lines
9.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cron_test
import (
"context"
"sync"
"testing"
"time"
"github.com/EthanCodeCraft/xlgo-core/cron"
)
// ============================================================
// C12arunTask 计数写入无锁 + GetTask/ListTasks 返回 live 指针 → data race
// ============================================================
// TestC12aConcurrentReadWriteNoRace 验证并发 RunTask/GetTask/ListTasks + 调度运行
// 无数据竞争(-race)。
//
// 修复前:runTask 无锁写 LastRun/RunCountGetTask/ListTasks 返回 live 指针并发读 →
// -race 必采到 DATA RACE。修复后:写入纳入锁、Getter 返回拷贝。
func TestC12aConcurrentReadWriteNoRace(t *testing.T) {
scheduler := cron.NewScheduler()
scheduler.AddTask("t1", cron.Every(50*time.Millisecond), func(ctx context.Context) error {
return nil
})
scheduler.AddTask("t2", cron.Every(80*time.Millisecond), func(ctx context.Context) error {
return nil
})
scheduler.Start()
t.Cleanup(scheduler.Stop)
stop := make(chan struct{})
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
}
// 读取返回拷贝的字段——若写侧无锁,与此处读竞争(C12a)。
if g, err := scheduler.GetTask("t1"); err == nil {
_ = g.RunCount
_ = g.LastRun
}
for _, tk := range scheduler.ListTasks() {
_ = tk.RunCount
_ = tk.LastRun
}
}
}()
}
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
}
_ = scheduler.RunTask("t2")
}
}()
time.Sleep(300 * time.Millisecond)
close(stop)
wg.Wait()
}
// TestC12aGetTaskReturnsCopy 验证 GetTask 返回拷贝,修改返回值不影响内部状态。
func TestC12aGetTaskReturnsCopy(t *testing.T) {
scheduler := cron.NewScheduler()
scheduler.AddTask("t", cron.Every(time.Minute), func(ctx context.Context) error {
return nil
})
got, err := scheduler.GetTask("t")
if err != nil {
t.Fatalf("GetTask: %v", err)
}
got.RunCount = 9999
got.Enabled = false
again, _ := scheduler.GetTask("t")
if again.RunCount == 9999 {
t.Error("GetTask returned live pointer (RunCount mutated internally)")
}
if !again.Enabled {
t.Error("GetTask returned live pointer (Enabled mutated internally)")
}
}
// TestC12aListTasksReturnsCopies 验证 ListTasks 元素为拷贝。
func TestC12aListTasksReturnsCopies(t *testing.T) {
scheduler := cron.NewScheduler()
scheduler.AddTask("t", cron.Every(time.Minute), func(ctx context.Context) error {
return nil
})
list := scheduler.ListTasks()
list[0].RunCount = 777
again := scheduler.ListTasks()
if again[0].RunCount == 777 {
t.Error("ListTasks returned live pointer (RunCount mutated internally)")
}
}
// ============================================================
// C12b:长任务跨 tick 重叠执行
// ============================================================
// TestC12bRunTaskConcurrentManualTriggerReturnsError 验证手动 RunTask 占用守卫期间,
// 再次 RunTask 返"任务正在执行中"错误。
func TestC12bRunTaskConcurrentManualTriggerReturnsError(t *testing.T) {
scheduler := cron.NewScheduler()
started := make(chan struct{})
release := make(chan struct{})
scheduler.AddTask("block", cron.Every(time.Hour), func(ctx context.Context) error {
close(started)
<-release
return nil
})
var firstErr error
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
firstErr = scheduler.RunTask("block")
}()
<-started
secondErr := scheduler.RunTask("block")
if secondErr == nil {
t.Error("second RunTask should fail while first is running (C12b running guard)")
}
close(release)
wg.Wait()
if firstErr != nil {
t.Errorf("first RunTask error: %v", firstErr)
}
}
// ============================================================
// C12d:Weekly 当天未到点目标被跳一周
// ============================================================
func TestC12dWeeklySameDayBeforeTarget(t *testing.T) {
schedule := cron.WeeklySchedule{Day: time.Monday, Hour: 9, Minute: 0}
// 周一 8:00(目标 9:00 未到)→ 应返回本周一 9:00,不跳周。
now := time.Date(2026, 6, 29, 8, 0, 0, 0, time.UTC) // 2026-06-29 是周一
if now.Weekday() != time.Monday {
t.Fatalf("test fixture: expected Monday, got %v", now.Weekday())
}
next := schedule.Next(now)
want := time.Date(2026, 6, 29, 9, 0, 0, 0, time.UTC)
if !next.Equal(want) {
t.Errorf("Weekly same-day-before-target: got %v, want %v (C12d skip-week bug)", next, want)
}
}
func TestC12dWeeklySameDayAfterTarget(t *testing.T) {
schedule := cron.WeeklySchedule{Day: time.Monday, Hour: 9, Minute: 0}
// 周一 10:00(目标 9:00 已过)→ 下周一 9:00。
now := time.Date(2026, 6, 29, 10, 0, 0, 0, time.UTC)
next := schedule.Next(now)
want := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC)
if !next.Equal(want) {
t.Errorf("Weekly same-day-after-target: got %v, want %v", next, want)
}
}
func TestC12dWeeklyCrossWeek(t *testing.T) {
schedule := cron.WeeklySchedule{Day: time.Monday, Hour: 9, Minute: 0}
// 周三 → 下周一。
now := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC) // 周三
if now.Weekday() != time.Wednesday {
t.Fatalf("test fixture: expected Wednesday, got %v", now.Weekday())
}
next := schedule.Next(now)
want := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC)
if !next.Equal(want) {
t.Errorf("Weekly cross-week: got %v, want %v", next, want)
}
}
// ============================================================
// C12ecron 解析缺陷(周日 7 / 范围环绕 / 严格解析)
// 1-5,8 / garbage / */garbage 的 matchField 直接测试见 cron_c12_internal_test.go
// ============================================================
func TestC12eWeekdaySundayAs7(t *testing.T) {
schedule := cron.FullCronSchedule{
Minute: "0",
Hour: "0",
Day: "*",
Month: "*",
Weekday: "7",
}
// 0 0 * * 7 → 每周日凌晨。从周六 23:00 找下一个匹配应落周日 00:00。
now := time.Date(2026, 6, 27, 23, 0, 0, 0, time.UTC) // 周六
if now.Weekday() != time.Saturday {
t.Fatalf("test fixture: expected Saturday, got %v", now.Weekday())
}
next := schedule.Next(now)
if next.Weekday() != time.Sunday {
t.Errorf("0 0 * * 7 should land on Sunday, got %v (C12e 7≠Sunday)", next.Weekday())
}
if next.Hour() != 0 || next.Minute() != 0 {
t.Errorf("0 0 * * 7 should land on 00:00, got %v", next)
}
}
func TestC12eWeekdayRangeWraparound(t *testing.T) {
// "6-1" 在 weekday 环绕:周六(6)、周日(0)、周一(1)。
schedule := cron.FullCronSchedule{
Minute: "0",
Hour: "0",
Day: "*",
Month: "*",
Weekday: "6-1",
}
for _, w := range []time.Weekday{time.Saturday, time.Sunday, time.Monday} {
tt := time.Date(2026, 6, 29, 0, 0, 0, 0, time.UTC) // 周一
for tt.Weekday() != w {
tt = tt.AddDate(0, 0, 1)
}
next := schedule.Next(tt.Add(-1 * time.Minute))
if next.Weekday() != w {
t.Errorf("6-1 should match %v, got %v", w, next.Weekday())
}
}
// 周二不应被 6-1 匹配,下一个应是周六。
tt := time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC) // 周二
if tt.Weekday() != time.Tuesday {
t.Fatalf("test fixture: expected Tuesday, got %v", tt.Weekday())
}
next := schedule.Next(tt.Add(-1 * time.Minute))
if next.Weekday() != time.Saturday {
t.Errorf("6-1 should skip Tuesday, next match %v, want Saturday", next.Weekday())
}
}
// TestC12eParseCronStrict 验证严格解析。
func TestC12eParseCronStrict(t *testing.T) {
cases := []struct {
expr string
ok bool
}{
{"0 12 * * *", true},
{"*/15 * * * *", true},
{"0 9-17 * * 1-5", true},
{"0 0 1 * 7", true}, // 周日 7 合法
{"0 0 * * 0-7", false},
{"invalid", false},
{"1-5,8 0 * * *", true},
{"60 0 * * *", false}, // 分钟越界
{"0 25 * * *", false}, // 小时越界
{"0 0 0 * *", false}, // 日越界
{"0 0 * 13 *", false}, // 月越界
{"0 0 * * 9", false}, // 周越界
{"garbage 0 * * *", false},
{"*/0 * * * *", false}, // step=0 非法
}
for _, c := range cases {
_, err := cron.ParseCronStrict(c.expr)
if c.ok && err != nil {
t.Errorf("ParseCronStrict(%q) unexpected error: %v", c.expr, err)
}
if !c.ok && err == nil {
t.Errorf("ParseCronStrict(%q) expected error, got nil", c.expr)
}
}
}
// TestC12eParseCronFallback verifies ParseCron now fails fast on invalid input.
func TestC12eParseCronFallback(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("ParseCron(invalid) should panic")
}
}()
_ = cron.ParseCron("invalid")
}
func TestC12eParseCronOrDefaultFallback(t *testing.T) {
s := cron.ParseCronOrDefault("invalid")
if s.Minute != "*" || s.Hour != "*" || s.Day != "*" || s.Month != "*" || s.Weekday != "*" {
t.Errorf("ParseCronOrDefault(invalid) should fall back to all-*, got %+v", s)
}
// 合法表达式不回退。
s = cron.ParseCron("1-5,8 0 * * *")
if s.Minute != "1-5,8" {
t.Errorf("ParseCron valid: Minute = %q, want 1-5,8", s.Minute)
}
}
// TestC12eCronScheduleGarbageNoMatch 验证简化 Cron 不再 parseInt 容错为 0。
func TestC12eCronScheduleGarbageNoMatch(t *testing.T) {
bad := cron.CronSchedule{Minute: "garbage", Hour: "9"}
// garbage → splitPattern 返空 → matchMinute 返 false → 24h 内无匹配。
now := time.Date(2026, 6, 29, 8, 0, 0, 0, time.UTC)
next := bad.Next(now)
if !next.IsZero() {
t.Errorf("CronSchedule garbage minute should not match, got next=%v (C12e)", next)
}
}