136 lines
4.2 KiB
Go
136 lines
4.2 KiB
Go
package xlgo_test
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
|
||
xlgo "github.com/EthanCodeCraft/xlgo-core"
|
||
"github.com/EthanCodeCraft/xlgo-core/cron"
|
||
)
|
||
|
||
// TestAppCronSchedulerInstance 固化 Phase 3:WithCronTask 把任务注册到 App 专属调度器,
|
||
// app.Scheduler() 返回该实例且含注册的任务。
|
||
func TestAppCronSchedulerInstance(t *testing.T) {
|
||
saved := cron.SwapDefaultScheduler(cron.NewScheduler())
|
||
defer cron.SwapDefaultScheduler(saved)
|
||
|
||
app := xlgo.New(
|
||
xlgo.WithConfig(testConfig(18112)),
|
||
xlgo.WithCronTask("my-task", cron.Every(time.Hour), func(context.Context) error { return nil }),
|
||
)
|
||
if err := app.Init(); err != nil {
|
||
t.Fatalf("Init: %v", err)
|
||
}
|
||
defer app.Shutdown()
|
||
|
||
s := app.Scheduler()
|
||
if s == nil {
|
||
t.Fatal("app.Scheduler() = nil after Init with WithCronTask")
|
||
}
|
||
found := false
|
||
for _, tk := range s.ListTasks() {
|
||
if tk.Name == "my-task" {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatal("WithCronTask task not registered on App scheduler")
|
||
}
|
||
}
|
||
|
||
// TestAppCronRollbackRestoresDefault 固化 Phase 3 回滚:Init 失败后全局默认调度器
|
||
// 必须恢复为 Init 前的实例(不含 App 注册的任务),而非残留 App 的调度器。
|
||
func TestAppCronRollbackRestoresDefault(t *testing.T) {
|
||
saved := cron.SwapDefaultScheduler(cron.NewScheduler())
|
||
defer cron.SwapDefaultScheduler(saved)
|
||
|
||
app := xlgo.New(
|
||
xlgo.WithConfig(testConfig(18113)),
|
||
xlgo.WithCronTask("rollback-task", cron.Every(time.Hour), func(context.Context) error { return nil }),
|
||
xlgo.WithHook(xlgo.Hook{Name: "fail", OnInit: func(*xlgo.App) error { return errors.New("boom") }}),
|
||
)
|
||
if err := app.Init(); err == nil {
|
||
_ = app.Shutdown()
|
||
t.Fatal("expected Init to fail via OnInit hook")
|
||
}
|
||
|
||
// 回滚后全局默认不应含 App 的 "rollback-task"
|
||
for _, tk := range cron.GetScheduler().ListTasks() {
|
||
if tk.Name == "rollback-task" {
|
||
t.Fatal("rollback did not restore global default scheduler (App task still on global)")
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestAppCronMultiAppIsolation 固化 Phase 3 核心契约:单进程多 App 时,App A 的 Shutdown
|
||
// 只停 A 自己的调度器,不影响 App B 的调度器继续运行(修复前 cron 是全局单例,A.Shutdown
|
||
// 会经 cron.StopGlobalWithTimeout 把 B 的调度器也停了)。
|
||
//
|
||
// 调度器内部 ticker 为 1s,故任务每 1s tick 触发一次(schedule=Every(1ms) 仅决定 due 判定,
|
||
// 实际 spawn 受 1s ticker 节拍)。测试因此需要跨 1s tick 观测。
|
||
func TestAppCronMultiAppIsolation(t *testing.T) {
|
||
saved := cron.SwapDefaultScheduler(cron.NewScheduler())
|
||
defer cron.SwapDefaultScheduler(saved)
|
||
|
||
var ranA, ranB atomic.Int32
|
||
appA := xlgo.New(
|
||
xlgo.WithConfig(testConfig(18110)),
|
||
xlgo.WithCronTask("taskA", cron.Every(time.Millisecond), func(context.Context) error {
|
||
ranA.Add(1)
|
||
return nil
|
||
}),
|
||
)
|
||
appB := xlgo.New(
|
||
xlgo.WithConfig(testConfig(18111)),
|
||
xlgo.WithCronTask("taskB", cron.Every(time.Millisecond), func(context.Context) error {
|
||
ranB.Add(1)
|
||
return nil
|
||
}),
|
||
)
|
||
if err := appA.Init(); err != nil {
|
||
t.Fatalf("appA Init: %v", err)
|
||
}
|
||
if err := appB.Init(); err != nil {
|
||
t.Fatalf("appB Init: %v", err)
|
||
}
|
||
defer appB.Shutdown()
|
||
|
||
// 轮询等第一个 1s tick:两个调度器都应各跑至少一次(deadline 3s,容忍慢机/CI 漂移)
|
||
deadline := time.Now().Add(3 * time.Second)
|
||
for time.Now().Before(deadline) {
|
||
if ranA.Load() > 0 && ranB.Load() > 0 {
|
||
break
|
||
}
|
||
time.Sleep(50 * time.Millisecond)
|
||
}
|
||
aAtTick := ranA.Load()
|
||
bAtTick := ranB.Load()
|
||
if aAtTick == 0 || bAtTick == 0 {
|
||
t.Fatalf("expected both schedulers ran at least once: A=%d B=%d", aAtTick, bAtTick)
|
||
}
|
||
|
||
// 停 App A
|
||
if err := appA.Shutdown(); err != nil {
|
||
t.Fatalf("appA Shutdown: %v", err)
|
||
}
|
||
|
||
// 轮询等 B 再跑一次(deadline 3s),证明 B 调度器未被 A.Shutdown 停掉
|
||
deadline = time.Now().Add(3 * time.Second)
|
||
for time.Now().Before(deadline) {
|
||
if ranB.Load() > bAtTick {
|
||
break
|
||
}
|
||
time.Sleep(50 * time.Millisecond)
|
||
}
|
||
if ranA.Load() != aAtTick {
|
||
t.Errorf("App A scheduler ran after its Shutdown (not isolated): before=%d after=%d", aAtTick, ranA.Load())
|
||
}
|
||
if ranB.Load() <= bAtTick {
|
||
t.Errorf("App B scheduler did not continue after App A Shutdown (not isolated): before=%d after=%d", bAtTick, ranB.Load())
|
||
}
|
||
}
|