package reconcile_test import ( "context" "errors" "sync/atomic" "testing" "time" "github.com/wangjia/pay/internal/reconcile" ) func TestRunnerRunOnceExecutesAllAndRecoversPanic(t *testing.T) { r := reconcile.NewRunner() var a, b int r.Add("inc-a", time.Minute, func(context.Context) error { a++; return nil }) r.Add("boom", time.Minute, func(context.Context) error { panic("kaboom") }) // 不得拖垮后续 r.Add("inc-b", time.Minute, func(context.Context) error { b++; return errors.New("soft") }) r.RunOnce(context.Background()) // panic 被 recover,error 被记录,均不中断 if a != 1 || b != 1 { t.Fatalf("a=%d b=%d, want 1/1(panic 任务不应阻断其它)", a, b) } } // TestRunnerZeroIntervalSkipsRegistrationAndStartDoesNotPanic 覆盖 Important #1: // operator 配 interval<=0(如 expire_every_sec: 0)不得让 Start 里 time.NewTicker // panic 崩进程 —— Add/AddLocal 应在注册期就 skip 该任务(不进 r.tasks),Start 对它 // 不会创建 ticker,自然不 panic;RunOnce/RunOnceLocal 也因任务未注册而不执行它。 func TestRunnerZeroIntervalSkipsRegistrationAndStartDoesNotPanic(t *testing.T) { r := reconcile.NewRunner() var zeroRan, negRan, okRan int32 r.Add("zero-interval", 0, func(context.Context) error { atomic.AddInt32(&zeroRan, 1) return nil }) r.AddLocal("neg-interval", -time.Second, func(context.Context) error { atomic.AddInt32(&negRan, 1) return nil }) r.Add("ok-interval", 5*time.Millisecond, func(context.Context) error { atomic.AddInt32(&okRan, 1) return nil }) r.RunOnce(context.Background()) if zeroRan != 0 || negRan != 0 { t.Fatalf("interval<=0 的任务不应被注册/执行: zeroRan=%d negRan=%d", zeroRan, negRan) } if okRan != 1 { t.Fatalf("正常任务应正常执行一次, got okRan=%d", okRan) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan struct{}) go func() { defer close(done) r.Start(ctx) // 若 zero-interval/neg-interval 仍被注册,NewTicker(<=0) 会 panic 崩掉这个 goroutine }() time.Sleep(30 * time.Millisecond) // 留时间给 ok-interval 的 ticker 至少 tick 一次 cancel() <-done if atomic.LoadInt32(&zeroRan) != 0 || atomic.LoadInt32(&negRan) != 0 { t.Fatalf("Start 之后 interval<=0 的任务仍不应执行: zeroRan=%d negRan=%d", zeroRan, negRan) } } // TestRunnerRunOnceLocalOnlyRunsLocalTasks 覆盖 Important #2:启动预热 // RunOnceLocal 只应执行 AddLocal 注册的任务,Add(默认网络型)注册的任务不跑, // 交给 Start 里各自 ticker 的首跳。 func TestRunnerRunOnceLocalOnlyRunsLocalTasks(t *testing.T) { r := reconcile.NewRunner() var ran []string r.AddLocal("local-a", time.Minute, func(context.Context) error { ran = append(ran, "local-a") return nil }) r.Add("network-b", time.Minute, func(context.Context) error { ran = append(ran, "network-b") return nil }) r.AddLocal("local-c", time.Minute, func(context.Context) error { ran = append(ran, "local-c") return nil }) r.RunOnceLocal(context.Background()) if len(ran) != 2 { t.Fatalf("RunOnceLocal 应只跑 2 个 Local 任务, got %v", ran) } for _, name := range ran { if name == "network-b" { t.Fatalf("RunOnceLocal 不应执行网络型任务, got %v", ran) } } }