项目文件夹

文件
2026-07-14 10:24:10 +08:00

322 行
9.3 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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/RunCount,GetTask/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)
}
}