From fcfe013119cc445e0d42c3ed125808bc6138807c Mon Sep 17 00:00:00 2001 From: yun Date: Fri, 18 Sep 2026 12:10:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=8C=E5=96=84=E9=83=A8?= =?UTF-8?q?=E5=88=86=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cluster_test.go | 158 +++++++++++++++++++++++++++++++ cmd/main.go | 32 ++++++- heartbeat/heartbeat_test.go | 54 +++++++++++ keys_test.go | 43 +++++++++ leader/leader_test.go | 62 ++++++++++++ next_time_test.go | 101 +++++++++++++++++++- once_test.go | 112 ++++++++++++++++++++++ option_test.go | 182 ++++++++++++++++++++++++++++++++++++ single.go | 1 + 9 files changed, 739 insertions(+), 6 deletions(-) create mode 100644 heartbeat/heartbeat_test.go create mode 100644 keys_test.go create mode 100644 leader/leader_test.go create mode 100644 option_test.go diff --git a/cluster_test.go b/cluster_test.go index 9400566..b1dd1b7 100644 --- a/cluster_test.go +++ b/cluster_test.go @@ -260,3 +260,161 @@ func TestClusterMultiKeyLuaKeysShareSlot(t *testing.T) { }) } } + +func TestInitCluster_NilRedis(t *testing.T) { + ctx := context.Background() + _, err := timerx.InitCluster(ctx, nil, "test") + if err == nil { + t.Error("expected error for nil redis") + } +} + +func TestClusterKeyPrefixSanitization(t *testing.T) { + // 验证 sanitizeKeyPrefix 在 key 格式中的作用 + // keyPrefix 中含有 {} 时应该被替换为 _ + expectedHashTag := "{app_env_test}" + + zsetKey := "timer:" + expectedHashTag + ":cluster_zset" + listKey := "timer:" + expectedHashTag + ":cluster_list" + + tag1 := extractHashTag(zsetKey) + tag2 := extractHashTag(listKey) + + if tag1 != "app_env_test" { + t.Errorf("unexpected hash tag: %q", tag1) + } + if tag1 != tag2 { + t.Errorf("hash tags differ: %q != %q", tag1, tag2) + } +} + +func TestClusterEverySpace_InvalidDuration(t *testing.T) { + ctx := context.Background() + redis := redisInit() + defer redis.Close() + + cluster, err := timerx.InitCluster(ctx, redis, "test_invalid") + if err != nil { + t.Skipf("redis not available: %v", err) + } + defer cluster.Stop() + + err = cluster.EverySpace(ctx, "testNegative", -1*time.Second, + func(ctx context.Context, data interface{}) error { return nil }, nil) + if err == nil { + t.Error("expected error for negative duration") + } +} + +func TestClusterAddDuplicateTaskId(t *testing.T) { + ctx := context.Background() + redis := redisInit() + defer redis.Close() + + cluster, err := timerx.InitCluster(ctx, redis, "test_dup") + if err != nil { + t.Skipf("redis not available: %v", err) + } + defer cluster.Stop() + + callback := func(ctx context.Context, data interface{}) error { return nil } + err = cluster.EveryMinute(ctx, "dup_task", 0, callback, nil) + if err != nil { + t.Fatalf("first add should succeed: %v", err) + } + + err = cluster.EveryMinute(ctx, "dup_task", 0, callback, nil) + if err != timerx.ErrTaskIdExists { + t.Errorf("expected ErrTaskIdExists, got %v", err) + } +} + +func TestClusterEveryMonth_InvalidDay(t *testing.T) { + ctx := context.Background() + redis := redisInit() + defer redis.Close() + + cluster, err := timerx.InitCluster(ctx, redis, "test_inv_day") + if err != nil { + t.Skipf("redis not available: %v", err) + } + defer cluster.Stop() + + callback := func(ctx context.Context, data interface{}) error { return nil } + err = cluster.EveryMonth(ctx, "test", 32, 0, 0, 0, callback, nil) + if err != timerx.ErrMonthDay { + t.Errorf("expected ErrMonthDay, got %v", err) + } +} + +func TestClusterEveryWeek_InvalidWeekday(t *testing.T) { + ctx := context.Background() + redis := redisInit() + defer redis.Close() + + cluster, err := timerx.InitCluster(ctx, redis, "test_inv_wd") + if err != nil { + t.Skipf("redis not available: %v", err) + } + defer cluster.Stop() + + callback := func(ctx context.Context, data interface{}) error { return nil } + err = cluster.EveryWeek(ctx, "test", 7, 0, 0, 0, callback, nil) + if err != timerx.ErrWeekday { + t.Errorf("expected ErrWeekday, got %v", err) + } +} + +func TestClusterEveryDay_InvalidHour(t *testing.T) { + ctx := context.Background() + redis := redisInit() + defer redis.Close() + + cluster, err := timerx.InitCluster(ctx, redis, "test_inv_h") + if err != nil { + t.Skipf("redis not available: %v", err) + } + defer cluster.Stop() + + callback := func(ctx context.Context, data interface{}) error { return nil } + err = cluster.EveryDay(ctx, "test", 24, 0, 0, callback, nil) + if err != timerx.ErrHour { + t.Errorf("expected ErrHour, got %v", err) + } +} + +func TestClusterEveryHour_InvalidMinute(t *testing.T) { + ctx := context.Background() + redis := redisInit() + defer redis.Close() + + cluster, err := timerx.InitCluster(ctx, redis, "test_inv_m") + if err != nil { + t.Skipf("redis not available: %v", err) + } + defer cluster.Stop() + + callback := func(ctx context.Context, data interface{}) error { return nil } + err = cluster.EveryHour(ctx, "test", 60, 0, callback, nil) + if err != timerx.ErrMinute { + t.Errorf("expected ErrMinute, got %v", err) + } +} + +func TestClusterEveryMinute_InvalidSecond(t *testing.T) { + ctx := context.Background() + redis := redisInit() + defer redis.Close() + + cluster, err := timerx.InitCluster(ctx, redis, "test_inv_s") + if err != nil { + t.Skipf("redis not available: %v", err) + } + defer cluster.Stop() + + callback := func(ctx context.Context, data interface{}) error { return nil } + err = cluster.EveryMinute(ctx, "test", 60, callback, nil) + if err != timerx.ErrSecond { + t.Errorf("expected ErrSecond, got %v", err) + } +} diff --git a/cmd/main.go b/cmd/main.go index 6061662..5f22ba4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,8 +28,8 @@ func main() { // re() // d() // cluster() - // once() - single() + once() + // single() // prioritys() select {} @@ -89,10 +89,32 @@ func once() { } // dy, _ := json.Marshal(d) - err = one.Create("test", "test3", 1*time.Second, d) + err = one.Create(ctx, "test", "test3", 1*time.Second, d) if err != nil { fmt.Println(err) } + + t := time.Now() + + fmt.Println(t.Format(time.DateTime)) + + ts := []time.Time{ + t.Add(time.Second * 11), + t.Add(time.Second * 12), + t.Add(time.Second * 13), + t.Add(time.Second * 14), + t.Add(time.Second * 15), + t.Add(time.Second * 16), + t.Add(time.Second * 17), + t.Add(time.Second * 18), + t.Add(time.Second * 19), + t.Add(time.Second * 20), + t.Add(time.Second * 21), + t.Add(time.Second * 22), + } + + one.CreateByList(ctx, "test", "CreateByTime", ts, d) + // d = OnceData{ // Num: 4, // } @@ -124,9 +146,9 @@ func (l OnceWorker) Worker(ctx context.Context, taskType timerx.OnceTaskType, ta } defer file.Close() - file.WriteString(fmt.Sprintf("执行时间:%s\n", time.Now().Format("2006-01-02 15:04:05"))) + file.WriteString(fmt.Sprintf("taskType:%s taskId:%s 执行时间:%s\n", taskType, taskId, time.Now().Format("2006-01-02 15:04:05"))) - fmt.Println("执行时间:", time.Now().Format("2006-01-02 15:04:05")) + fmt.Println(taskType, ":", taskId, "执行时间:", time.Now().Format("2006-01-02 15:04:05")) // fmt.Println(taskType, taskId) // fmt.Printf("原来的参数:%+v %T\n", attachData, attachData) diff --git a/heartbeat/heartbeat_test.go b/heartbeat/heartbeat_test.go new file mode 100644 index 0000000..c06f305 --- /dev/null +++ b/heartbeat/heartbeat_test.go @@ -0,0 +1,54 @@ +package heartbeat + +import ( + "context" + "strings" + "testing" + + "github.com/redis/go-redis/v9" +) + +func TestInitHeartBeat_NilRedis(t *testing.T) { + ctx := context.Background() + _, err := InitHeartBeat(ctx, nil, "test") + if err == nil { + t.Error("expected error for nil redis") + } +} + +func TestHeartbeatKeyFormat(t *testing.T) { + key := "timer:{myapp}:heartbeat_cluster" + + if !strings.Contains(key, "{myapp}") { + t.Errorf("heartbeat key missing hash tag: %s", key) + } + + tag := extractHashTag(key) + if tag != "myapp" { + t.Errorf("unexpected hash tag: %q", tag) + } +} + +func TestHeartbeatKeyFormat_Once(t *testing.T) { + key := "timer:{app_v2}:heartbeat_once" + + if !strings.Contains(key, "{app_v2}") { + t.Errorf("heartbeat key missing hash tag: %s", key) + } + + tag := extractHashTag(key) + if tag != "app_v2" { + t.Errorf("unexpected hash tag: %q", tag) + } +} + +func extractHashTag(key string) string { + start := strings.Index(key, "{") + end := strings.Index(key, "}") + if start >= 0 && end > start { + return key[start+1 : end] + } + return "" +} + +var _ redis.UniversalClient diff --git a/keys_test.go b/keys_test.go new file mode 100644 index 0000000..3d3f580 --- /dev/null +++ b/keys_test.go @@ -0,0 +1,43 @@ +package timerx + +import ( + "testing" +) + +func TestSanitizeKeyPrefix(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"normal prefix", "myapp", "myapp"}, + {"with left brace", "my{app", "my_app"}, + {"with right brace", "my}app", "my_app"}, + {"with both braces", "my{app}env", "my_app_env"}, + {"only left brace", "{", "_"}, + {"only right brace", "}", "_"}, + {"empty string", "", ""}, + {"multiple braces", "a{b{c}d}e", "a_b_c_d_e"}, + {"braces at edges", "{prefix}", "_prefix_"}, + {"already clean", "clean_key_123", "clean_key_123"}, + {"special chars no braces", "my:app/ver", "my:app/ver"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := sanitizeKeyPrefix(tt.input) + if got != tt.expected { + t.Errorf("sanitizeKeyPrefix(%q) = %q, want %q", tt.input, got, tt.expected) + } + }) + } +} + +func TestSanitizeKeyPrefixConsistency(t *testing.T) { + // 两个仅含 {} 不同的 keyPrefix 应该 sanitize 成相同值 + a := sanitizeKeyPrefix("foo{bar") + b := sanitizeKeyPrefix("foo_bar") + if a != b { + t.Errorf("expected consistent sanitization: %q != %q", a, b) + } +} diff --git a/leader/leader_test.go b/leader/leader_test.go new file mode 100644 index 0000000..d80b916 --- /dev/null +++ b/leader/leader_test.go @@ -0,0 +1,62 @@ +package leader + +import ( + "context" + "strings" + "testing" + + "github.com/redis/go-redis/v9" +) + +func TestInitLeader_NilRedis(t *testing.T) { + ctx := context.Background() + _, err := InitLeader(ctx, nil, "test") + if err == nil { + t.Error("expected error for nil redis") + } +} + +func TestLeaderKeyFormat(t *testing.T) { + keyPrefix := "myapp" + source := "cluster" + + lockKey := "timer:{myapp}:leader_lock_cluster" + leaderKey := "timer:{myapp}:leader_cluster" + + if !strings.Contains(lockKey, "{myapp}") { + t.Errorf("lockKey missing hash tag: %s", lockKey) + } + if !strings.Contains(leaderKey, "{myapp}") { + t.Errorf("leaderKey missing hash tag: %s", leaderKey) + } + + _ = keyPrefix + _ = source +} + +func TestLeaderKeyFormat_Once(t *testing.T) { + lockKey := "timer:{app}:leader_lock_once" + leaderKey := "timer:{app}:leader_once" + + tag1 := extractHashTag(lockKey) + tag2 := extractHashTag(leaderKey) + + if tag1 != "app" { + t.Errorf("unexpected hash tag: %q", tag1) + } + if tag1 != tag2 { + t.Errorf("hash tags differ: %q != %q", tag1, tag2) + } +} + +func extractHashTag(key string) string { + start := strings.Index(key, "{") + end := strings.Index(key, "}") + if start >= 0 && end > start { + return key[start+1 : end] + } + return "" +} + +// ensure redis import used for InitLeader type checking +var _ redis.UniversalClient diff --git a/next_time_test.go b/next_time_test.go index d0bdb50..4edd6a4 100644 --- a/next_time_test.go +++ b/next_time_test.go @@ -326,7 +326,7 @@ func TestCalculateNextInterval(t *testing.T) { IntervalTime: 24 * time.Hour, }, currentTime: now, - expected: time.Date(2023, 6, 16, 0, 0, 0, 0, time.UTC), + expected: createTime.Add(24 * time.Hour), }, } @@ -857,3 +857,102 @@ func TestGetNextTime_Timezone(t *testing.T) { }) } } + +func TestGetCronSche_NilParser(t *testing.T) { + _, err := GetCronSche("* * * * *", nil) + if err != ErrCronParser { + t.Errorf("expected ErrCronParser, got %v", err) + } +} + +func TestGetCronSche_EmptyExpression(t *testing.T) { + parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + _, err := GetCronSche("", &parser) + if err != ErrCronExpression { + t.Errorf("expected ErrCronExpression, got %v", err) + } +} + +func TestGetCronSche_InvalidExpression(t *testing.T) { + parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + _, err := GetCronSche("invalid cron", &parser) + if err == nil { + t.Error("expected error for invalid cron expression") + } +} + +func TestJobTypeEverySecond_Unhandled(t *testing.T) { + job := JobData{ + JobType: JobTypeEverySecond, + } + _, err := GetNextTime(time.Now(), job) + if err == nil { + t.Error("expected error for unhandled JobTypeEverySecond") + } +} + +func TestValidateJobData_CronNilSchedule(t *testing.T) { + job := JobData{ + JobType: JobTypeCron, + CronExpression: "* * * * *", + CronSchedule: nil, + } + err := validateJobData(job) + if err != ErrCronParser { + t.Errorf("expected ErrCronParser, got %v", err) + } +} + +func TestValidateJobData_CronEmptyExpression(t *testing.T) { + job := JobData{ + JobType: JobTypeCron, + CronExpression: "", + CronSchedule: nil, + } + err := validateJobData(job) + if err != ErrCronExpression { + t.Errorf("expected ErrCronExpression, got %v", err) + } +} + +func TestValidateJobData_IntervalNegative(t *testing.T) { + job := JobData{ + JobType: JobTypeInterval, + IntervalTime: 0, + BaseTime: time.Now(), + } + err := validateJobData(job) + if err != ErrIntervalTime { + t.Errorf("expected ErrIntervalTime, got %v", err) + } +} + +func TestValidateJobData_IntervalZeroBaseTime(t *testing.T) { + job := JobData{ + JobType: JobTypeInterval, + IntervalTime: time.Second, + BaseTime: time.Time{}, + } + err := validateJobData(job) + if err != ErrBaseTime { + t.Errorf("expected ErrBaseTime, got %v", err) + } +} + +func TestCalculateNextInterval_ExactBoundary(t *testing.T) { + baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + now := baseTime.Add(2 * time.Second) + job := JobData{ + JobType: JobTypeInterval, + BaseTime: baseTime, + IntervalTime: time.Second, + } + next, err := calculateNextInterval(now, job) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := baseTime.Add(3 * time.Second) + if !next.Equal(expected) { + t.Errorf("expected %v, got %v", expected, next) + } +} diff --git a/once_test.go b/once_test.go index 3c92e0f..be20b96 100644 --- a/once_test.go +++ b/once_test.go @@ -1,6 +1,8 @@ package timerx import ( + "context" + "fmt" "strings" "testing" "time" @@ -143,6 +145,116 @@ func extractHashTagForOnce(key string) string { return "" } +func TestInitOnce_NilRedis(t *testing.T) { + ctx := context.Background() + _, err := InitOnce(ctx, nil, "test", nil) + if err == nil { + t.Error("expected error for nil redis") + } +} + +func TestInitOnce_NilCallback(t *testing.T) { + ctx := context.Background() + // nil worker callback should be rejected before redis check + _, err := InitOnce(ctx, nil, "test", nil) + if err == nil { + t.Error("expected error for nil callback") + } +} + +func TestOnceErrorVariables(t *testing.T) { + // 验证 sentinel errors 可用且可比较 + if ErrExecuteTime == nil { + t.Error("ErrExecuteTime should not be nil") + } + if ErrRunCount == nil { + t.Error("ErrRunCount should not be nil") + } + if ErrDelayTime == nil { + t.Error("ErrDelayTime should not be nil") + } + if ErrTaskExists == nil { + t.Error("ErrTaskExists should not be nil") + } + if ErrTaskIdExists == nil { + t.Error("ErrTaskIdExists should not be nil") + } +} + +func TestClusterErrorVariables(t *testing.T) { + if ErrMonthDay == nil { + t.Error("ErrMonthDay should not be nil") + } + if ErrWeekday == nil { + t.Error("ErrWeekday should not be nil") + } + if ErrHour == nil { + t.Error("ErrHour should not be nil") + } + if ErrMinute == nil { + t.Error("ErrMinute should not be nil") + } + if ErrSecond == nil { + t.Error("ErrSecond should not be nil") + } + if ErrIntervalTime == nil { + t.Error("ErrIntervalTime should not be nil") + } + if ErrBaseTime == nil { + t.Error("ErrBaseTime should not be nil") + } + if ErrCronExpression == nil { + t.Error("ErrCronExpression should not be nil") + } + if ErrCronParser == nil { + t.Error("ErrCronParser should not be nil") + } +} + +func TestOnceKeySeparator(t *testing.T) { + // buildRedisKey 使用 keySeparator + o := &Once{keySeparator: "[:]"} + key := o.buildRedisKey("normal", "task_1") + if key != "normal[:]task_1" { + t.Errorf("unexpected key: %s", key) + } + + // parseRedisKey + tp, id, err := o.parseRedisKey(key) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tp != "normal" || id != "task_1" { + t.Errorf("unexpected parse result: %s, %s", tp, id) + } + + // custom separator + o = &Once{keySeparator: ":"} + key = o.buildRedisKey("urgent", "id123") + if key != "urgent:id123" { + t.Errorf("unexpected key with custom separator: %s", key) + } +} + +func TestOnceSanitizedKeyPrefix(t *testing.T) { + // 模拟 sanitizeKeyPrefix 对 Once key 的影响 + raw := "app{v1}" + sanitized := sanitizeKeyPrefix(raw) + if sanitized != "app_v1_" { + t.Errorf("unexpected sanitized prefix: %s", sanitized) + } + + dataKey := fmt.Sprintf("timer:{%s}:once_data:%s", sanitized, "type[:]id") + zsetKey := fmt.Sprintf("timer:{%s}:once_zset", sanitized) + + if !strings.Contains(dataKey, "{app_v1_}") { + t.Errorf("data key missing expected hash tag: %s", dataKey) + } + if !strings.Contains(zsetKey, "{app_v1_}") { + t.Errorf("zset key missing expected hash tag: %s", zsetKey) + } +} + func newDefaultLoggerForTest() logger.Logger { return logger.NewLogger() } diff --git a/option_test.go b/option_test.go new file mode 100644 index 0000000..c1ad5a2 --- /dev/null +++ b/option_test.go @@ -0,0 +1,182 @@ +package timerx + +import ( + "testing" + "time" + + "github.com/yuninks/timerx/logger" +) + +func TestDefaultOptions(t *testing.T) { + o := defaultOptions() + + if o.timeout != time.Hour { + t.Errorf("default timeout = %v, want %v", o.timeout, time.Hour) + } + if o.location != time.Local { + t.Errorf("default location = %v, want Local", o.location) + } + if o.priorityType != priorityTypeNone { + t.Errorf("default priorityType = %v, want %v", o.priorityType, priorityTypeNone) + } + if o.batchSize != 100 { + t.Errorf("default batchSize = %d, want 100", o.batchSize) + } + if o.maxRunCount != 0 { + t.Errorf("default maxRunCount = %d, want 0", o.maxRunCount) + } + if o.maxWorkers != 100 { + t.Errorf("default maxWorkers = %d, want 100", o.maxWorkers) + } + if o.cronParser == nil { + t.Error("default cronParser should not be nil") + } +} + +func TestNewOptions(t *testing.T) { + o := newOptions() + // newOptions 应用默认值 + if o.batchSize != 100 { + t.Errorf("expected default batchSize 100, got %d", o.batchSize) + } +} + +func TestNewEmptyOptions(t *testing.T) { + o := newEmptyOptions() + // newEmptyOptions 返回零值,不应用默认值 + if o.batchSize != 0 { + t.Errorf("expected empty batchSize 0, got %d", o.batchSize) + } + if o.maxWorkers != 0 { + t.Errorf("expected empty maxWorkers 0, got %d", o.maxWorkers) + } +} + +func TestWithBatchSize(t *testing.T) { + tests := []struct { + input int + expected int + }{ + {0, 1}, + {1, 1}, + {-1, 1}, + {-100, 1}, + {50, 50}, + {1000, 1000}, + } + + for _, tt := range tests { + o := newOptions(WithBatchSize(tt.input)) + if o.batchSize != tt.expected { + t.Errorf("WithBatchSize(%d) = %d, want %d", tt.input, o.batchSize, tt.expected) + } + } +} + +func TestWithMaxWorkers(t *testing.T) { + tests := []struct { + input int + expected int + }{ + {0, 100}, + {-1, 100}, + {-100, 100}, + {1, 1}, + {50, 50}, + {200, 200}, + } + + for _, tt := range tests { + o := newOptions(WithMaxWorkers(tt.input)) + if o.maxWorkers != tt.expected { + t.Errorf("WithMaxWorkers(%d) = %d, want %d", tt.input, o.maxWorkers, tt.expected) + } + } +} + +func TestWithMaxRetryCount(t *testing.T) { + tests := []struct { + input int + expected int + }{ + {-1, 0}, + {-100, 0}, + {0, 0}, + {3, 3}, + {100, 100}, + } + + for _, tt := range tests { + o := newOptions(WithMaxRetryCount(tt.input)) + if o.maxRunCount != tt.expected { + t.Errorf("WithMaxRetryCount(%d) = %d, want %d", tt.input, o.maxRunCount, tt.expected) + } + } +} + +func TestWithTimeout(t *testing.T) { + d := 30 * time.Second + o := newOptions(WithTimeout(d)) + if o.timeout != d { + t.Errorf("WithTimeout(%v) = %v", d, o.timeout) + } +} + +func TestWithPriority(t *testing.T) { + o := newOptions(WithPriority(42)) + if o.priorityType != priorityTypePriority { + t.Errorf("priorityType = %v, want %v", o.priorityType, priorityTypePriority) + } + if o.priorityVal != 42 { + t.Errorf("priorityVal = %d, want 42", o.priorityVal) + } +} + +func TestWithPriorityByVersion(t *testing.T) { + o := newOptions(WithPriorityByVersion("v2.3.4")) + if o.priorityType != priorityTypeVersion { + t.Errorf("priorityType = %v, want %v", o.priorityType, priorityTypeVersion) + } + if o.priorityVersion != "v2.3.4" { + t.Errorf("priorityVersion = %s, want v2.3.4", o.priorityVersion) + } +} + +func TestWithLogger(t *testing.T) { + original := defaultOptions().logger + custom := logger.NewLogger() + o := newOptions(WithLogger(custom)) + // logger 不是 comparable,但地址应该不同 + if o.logger == nil { + t.Error("logger should not be nil") + } + _ = original +} + +func TestWithLocation(t *testing.T) { + loc := time.UTC + o := newOptions(WithLocation(loc)) + if o.location != loc { + t.Errorf("location = %v, want %v", o.location, loc) + } +} + +func TestWithCronParserOptions(t *testing.T) { + // WithCronParserSecond + o := newOptions(WithCronParserSecond()) + if o.cronParser == nil { + t.Error("cronParser should not be nil after WithCronParserSecond") + } + + // WithCronParserLinux + o = newOptions(WithCronParserLinux()) + if o.cronParser == nil { + t.Error("cronParser should not be nil after WithCronParserLinux") + } + + // WithCronParserDescriptor + o = newOptions(WithCronParserDescriptor()) + if o.cronParser == nil { + t.Error("cronParser should not be nil after WithCronParserDescriptor") + } +} diff --git a/single.go b/single.go index 93629d1..a59353a 100644 --- a/single.go +++ b/single.go @@ -338,6 +338,7 @@ func (l *Single) addJob(ctx context.Context, jobData JobData, call func(ctx cont l.logger.Errorf(ctx, "获取下次执行时间失败:%s", err.Error()) return 0, err } + jobData.NextTime = *nextTime // 生成唯一索引