2026-05-14 00:40:49 +08:00
|
|
|
package task_test
|
2026-05-10 21:19:30 +08:00
|
|
|
|
|
|
|
|
import (
|
2026-05-14 00:40:49 +08:00
|
|
|
"context"
|
2026-05-10 21:19:30 +08:00
|
|
|
"errors"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
2026-05-14 00:40:49 +08:00
|
|
|
|
|
|
|
|
"apigo.cc/go/task"
|
|
|
|
|
"os"
|
2026-05-10 21:19:30 +08:00
|
|
|
)
|
|
|
|
|
|
2026-05-14 00:40:49 +08:00
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
|
task.DefaultScheduler.Start(context.Background(), nil)
|
|
|
|
|
os.Exit(m.Run())
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 21:19:30 +08:00
|
|
|
func TestTaskBasic(t *testing.T) {
|
|
|
|
|
var count int32
|
2026-05-14 00:40:49 +08:00
|
|
|
task.Add("test-basic", "@every 1s", func(ctx context.Context) error {
|
2026-05-10 21:19:30 +08:00
|
|
|
atomic.AddInt32(&count, 1)
|
2026-05-14 00:40:49 +08:00
|
|
|
return nil
|
2026-05-10 21:19:30 +08:00
|
|
|
})
|
|
|
|
|
time.Sleep(2500 * time.Millisecond)
|
2026-05-14 00:40:49 +08:00
|
|
|
|
2026-05-10 21:19:30 +08:00
|
|
|
if atomic.LoadInt32(&count) < 2 {
|
|
|
|
|
t.Errorf("Expected at least 2 runs, got %d", count)
|
|
|
|
|
}
|
2026-05-14 00:40:49 +08:00
|
|
|
task.Remove("test-basic")
|
2026-05-10 21:19:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTaskLifecycle(t *testing.T) {
|
|
|
|
|
var count int32
|
|
|
|
|
name := "test-lifecycle"
|
2026-05-14 00:40:49 +08:00
|
|
|
tk := task.Add(name, "@every 1s", func(ctx context.Context) error {
|
2026-05-10 21:19:30 +08:00
|
|
|
atomic.AddInt32(&count, 1)
|
2026-05-14 00:40:49 +08:00
|
|
|
return nil
|
2026-05-10 21:19:30 +08:00
|
|
|
})
|
2026-05-14 00:40:49 +08:00
|
|
|
|
|
|
|
|
tk.Disable()
|
2026-05-10 21:19:30 +08:00
|
|
|
time.Sleep(1500 * time.Millisecond)
|
|
|
|
|
if atomic.LoadInt32(&count) != 0 {
|
|
|
|
|
t.Errorf("Expected 0 runs while disabled, got %d", count)
|
|
|
|
|
}
|
2026-05-14 00:40:49 +08:00
|
|
|
|
|
|
|
|
tasks := task.List()
|
2026-05-10 21:19:30 +08:00
|
|
|
found := false
|
|
|
|
|
for _, item := range tasks {
|
|
|
|
|
if item.Name == name {
|
|
|
|
|
found = true
|
2026-05-14 00:40:49 +08:00
|
|
|
if item.Status != task.StatusDisabled {
|
2026-05-10 21:19:30 +08:00
|
|
|
t.Errorf("Expected status disabled, got %s", item.Status)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
t.Errorf("Task not found in list")
|
|
|
|
|
}
|
2026-05-14 00:40:49 +08:00
|
|
|
|
2026-05-10 21:19:30 +08:00
|
|
|
tk.Enable()
|
|
|
|
|
time.Sleep(2500 * time.Millisecond)
|
|
|
|
|
if atomic.LoadInt32(&count) == 0 {
|
|
|
|
|
t.Errorf("Expected task to run after enabling")
|
|
|
|
|
}
|
2026-05-14 00:40:49 +08:00
|
|
|
|
2026-05-10 21:19:30 +08:00
|
|
|
tk.Remove()
|
2026-05-14 00:40:49 +08:00
|
|
|
if task.Get(name) != nil {
|
2026-05-10 21:19:30 +08:00
|
|
|
t.Errorf("Expected task to be nil after remove")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTaskPolicySkip(t *testing.T) {
|
|
|
|
|
var count int32
|
2026-05-14 00:40:49 +08:00
|
|
|
task.Add("test-skip", "@every 1s", func(ctx context.Context) error {
|
2026-05-10 21:19:30 +08:00
|
|
|
atomic.AddInt32(&count, 1)
|
2026-05-14 00:40:49 +08:00
|
|
|
time.Sleep(1500 * time.Millisecond)
|
|
|
|
|
return nil
|
|
|
|
|
}, task.Config{Policy: task.PolicySkip})
|
|
|
|
|
|
|
|
|
|
time.Sleep(3500 * time.Millisecond)
|
|
|
|
|
|
2026-05-10 21:19:30 +08:00
|
|
|
if atomic.LoadInt32(&count) != 2 {
|
|
|
|
|
t.Errorf("Expected exactly 2 runs due to skip policy, got %d", count)
|
|
|
|
|
}
|
2026-05-14 00:40:49 +08:00
|
|
|
task.Remove("test-skip")
|
2026-05-10 21:19:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTaskTimeout(t *testing.T) {
|
2026-05-14 00:40:49 +08:00
|
|
|
var timeoutHit int32
|
|
|
|
|
task.Add("test-timeout", "@every 1s", func(ctx context.Context) error {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
|
|
|
|
atomic.AddInt32(&timeoutHit, 1)
|
|
|
|
|
}
|
|
|
|
|
return ctx.Err()
|
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}, task.Config{Timeout: 500 * time.Millisecond})
|
|
|
|
|
|
2026-05-10 21:19:30 +08:00
|
|
|
time.Sleep(2500 * time.Millisecond)
|
2026-05-14 00:40:49 +08:00
|
|
|
|
|
|
|
|
if atomic.LoadInt32(&timeoutHit) < 2 {
|
|
|
|
|
t.Errorf("Expected at least 2 timeout hits, got %d", timeoutHit)
|
2026-05-10 21:19:30 +08:00
|
|
|
}
|
2026-05-14 00:40:49 +08:00
|
|
|
task.Remove("test-timeout")
|
2026-05-10 21:19:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 00:40:49 +08:00
|
|
|
func TestTaskPanicRecover(t *testing.T) {
|
|
|
|
|
var count int32
|
|
|
|
|
task.Add("test-panic", "@every 1s", func(ctx context.Context) error {
|
|
|
|
|
atomic.AddInt32(&count, 1)
|
|
|
|
|
panic("boom")
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-10 21:19:30 +08:00
|
|
|
time.Sleep(2500 * time.Millisecond)
|
2026-05-14 00:40:49 +08:00
|
|
|
|
|
|
|
|
if atomic.LoadInt32(&count) < 2 {
|
|
|
|
|
t.Errorf("Expected at least 2 runs despite panics, got %d", count)
|
|
|
|
|
}
|
|
|
|
|
task.Remove("test-panic")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTaskManualRun(t *testing.T) {
|
|
|
|
|
var count int32
|
|
|
|
|
name := "test-manual"
|
|
|
|
|
tk := task.Add(name, "@every 1h", func(ctx context.Context) error {
|
|
|
|
|
atomic.AddInt32(&count, 1)
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Manual run via global function
|
|
|
|
|
task.Run(name)
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
if atomic.LoadInt32(&count) != 1 {
|
|
|
|
|
t.Errorf("Expected 1 run after manual Run, got %d", count)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Manual run via task object
|
|
|
|
|
tk.Run()
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
if atomic.LoadInt32(&count) != 2 {
|
|
|
|
|
t.Errorf("Expected 2 runs after manual Run, got %d", count)
|
|
|
|
|
}
|
|
|
|
|
task.Remove(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGracefulStop(t *testing.T) {
|
|
|
|
|
s := task.NewScheduler()
|
|
|
|
|
s.Start(context.Background(), nil)
|
|
|
|
|
|
|
|
|
|
var finished int32
|
|
|
|
|
// Use @every 1h to avoid auto-runs.
|
|
|
|
|
s.Add("test-graceful", "@every 1h", func(ctx context.Context) error {
|
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
|
atomic.AddInt32(&finished, 1)
|
|
|
|
|
return nil
|
|
|
|
|
}, task.Config{})
|
|
|
|
|
|
|
|
|
|
go s.Run("test-graceful") // Trigger manually
|
|
|
|
|
|
|
|
|
|
time.Sleep(200 * time.Millisecond) // Let it start
|
|
|
|
|
start := time.Now()
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer cancel()
|
2026-05-10 21:19:30 +08:00
|
|
|
|
2026-05-14 00:40:49 +08:00
|
|
|
s.Stop(ctx)
|
|
|
|
|
duration := time.Since(start)
|
|
|
|
|
|
|
|
|
|
if atomic.LoadInt32(&finished) != 1 {
|
|
|
|
|
t.Errorf("Expected exactly 1 task to finish during graceful stop, got %d", atomic.LoadInt32(&finished))
|
|
|
|
|
}
|
|
|
|
|
if duration < 500*time.Millisecond {
|
|
|
|
|
t.Errorf("Stop returned too quickly, didn't wait for task")
|
2026-05-10 21:19:30 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 00:40:49 +08:00
|
|
|
|
|
|
|
|
func BenchmarkTaskRegistration(b *testing.B) {
|
|
|
|
|
s := task.NewScheduler()
|
|
|
|
|
fn := func(ctx context.Context) error { return nil }
|
|
|
|
|
b.ResetTimer()
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
|
s.Add("bench", "@every 1h", fn, task.Config{})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BenchmarkTaskExecutionOverhead(b *testing.B) {
|
|
|
|
|
// Since we use cron, we can't easily benchmark the trigger frequency directly,
|
|
|
|
|
// but we can benchmark the runTask method which contains the wrapper overhead.
|
|
|
|
|
// We need to use unexported methods or reflection, or just benchmark a mocked run.
|
|
|
|
|
// Actually, let's just benchmark the Add + List + Get operations which are common.
|
|
|
|
|
s := task.NewScheduler()
|
|
|
|
|
fn := func(ctx context.Context) error { return nil }
|
|
|
|
|
s.Add("bench", "@every 1h", fn, task.Config{})
|
|
|
|
|
|
|
|
|
|
b.Run("Get", func(b *testing.B) {
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
|
s.Get("bench")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
b.Run("List", func(b *testing.B) {
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
|
s.List()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|