mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
f776611e29
* feat(cron): enhance CronService with wake channel and improve job scheduling logic * fix(cron): update file permission mode to use octal notation in test and fix some lint errors * fix(cron): improve wake channel handling and enhance concurrency in tests
238 lines
5.6 KiB
Go
238 lines
5.6 KiB
Go
package cron
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSaveStore_FilePermissions(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("file permission bits are not enforced on Windows")
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
storePath := filepath.Join(tmpDir, "cron", "jobs.json")
|
|
|
|
cs := NewCronService(storePath, nil)
|
|
|
|
_, err := cs.AddJob("test", CronSchedule{Kind: "every", EveryMS: int64Ptr(60000)}, "hello", false, "cli", "direct")
|
|
if err != nil {
|
|
t.Fatalf("AddJob failed: %v", err)
|
|
}
|
|
|
|
info, err := os.Stat(storePath)
|
|
if err != nil {
|
|
t.Fatalf("Stat failed: %v", err)
|
|
}
|
|
|
|
perm := info.Mode().Perm()
|
|
if perm != 0o600 {
|
|
t.Errorf("cron store has permission %04o, want 0600", perm)
|
|
}
|
|
}
|
|
|
|
func int64Ptr(v int64) *int64 {
|
|
return &v
|
|
}
|
|
|
|
func setupService(handler JobHandler) (*CronService, string) {
|
|
tmpFile := fmt.Sprintf("test_cron_%d.json", time.Now().UnixNano())
|
|
cs := NewCronService(tmpFile, handler)
|
|
return cs, tmpFile
|
|
}
|
|
|
|
func TestCronService_CRUD(t *testing.T) {
|
|
cs, path := setupService(nil)
|
|
defer os.Remove(path)
|
|
|
|
// Test AddJob
|
|
at := time.Now().Add(time.Hour).UnixMilli()
|
|
job, err := cs.AddJob("Task1", CronSchedule{Kind: "at", AtMS: &at}, "msg", true, "ch", "to")
|
|
if err != nil || job.ID == "" {
|
|
t.Fatalf("AddJob failed: %v", err)
|
|
}
|
|
|
|
// Test ListJobs
|
|
if len(cs.ListJobs(true)) != 1 {
|
|
t.Error("ListJobs should return 1 job")
|
|
}
|
|
|
|
// Test UpdateJob
|
|
job.Name = "UpdatedName"
|
|
err = cs.UpdateJob(job)
|
|
if err != nil || cs.store.Jobs[0].Name != "UpdatedName" {
|
|
t.Error("UpdateJob failed")
|
|
}
|
|
|
|
// Test EnableJob
|
|
cs.EnableJob(job.ID, false)
|
|
if cs.store.Jobs[0].Enabled != false || cs.store.Jobs[0].State.NextRunAtMS != nil {
|
|
t.Error("EnableJob(false) failed to clear state")
|
|
}
|
|
|
|
// Test RemoveJob
|
|
removed := cs.RemoveJob(job.ID)
|
|
if !removed || len(cs.store.Jobs) != 0 {
|
|
t.Error("RemoveJob failed")
|
|
}
|
|
}
|
|
|
|
// 2. Test Cron Expression Calculation Logic
|
|
func TestCronService_ComputeNextRun(t *testing.T) {
|
|
cs, path := setupService(nil)
|
|
defer os.Remove(path)
|
|
|
|
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli()
|
|
|
|
tests := []struct {
|
|
name string
|
|
schedule CronSchedule
|
|
wantNil bool
|
|
}{
|
|
{"Valid Cron", CronSchedule{Kind: "cron", Expr: "0 * * * *"}, false},
|
|
{"Invalid Cron", CronSchedule{Kind: "cron", Expr: "invalid"}, true},
|
|
{"Every MS", CronSchedule{Kind: "every", EveryMS: int64Ptr(5000)}, false},
|
|
{"At Future", CronSchedule{Kind: "at", AtMS: int64Ptr(now + 1000)}, false},
|
|
{"At Past", CronSchedule{Kind: "at", AtMS: int64Ptr(now - 1000)}, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := cs.computeNextRun(&tt.schedule, now)
|
|
if (got == nil) != tt.wantNil {
|
|
t.Errorf("%s: got %v, wantNil %v", tt.name, got, tt.wantNil)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 3. Test Execution Flow
|
|
func TestCronService_ExecutionFlow(t *testing.T) {
|
|
var mu sync.Mutex
|
|
executedJobs := make(map[string]bool)
|
|
|
|
handler := func(job *CronJob) (string, error) {
|
|
mu.Lock()
|
|
executedJobs[job.ID] = true
|
|
mu.Unlock()
|
|
return "ok", nil
|
|
}
|
|
|
|
cs, path := setupService(handler)
|
|
defer os.Remove(path)
|
|
|
|
// Start the service
|
|
if err := cs.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
defer cs.Stop()
|
|
|
|
// Add a job then runs 100ms from now
|
|
target := time.Now().Add(100 * time.Millisecond).UnixMilli()
|
|
job, _ := cs.AddJob("FastJob", CronSchedule{Kind: "at", AtMS: &target}, "", false, "", "")
|
|
|
|
// Check for job execution with a timeout
|
|
success := false
|
|
for range 20 {
|
|
mu.Lock()
|
|
if executedJobs[job.ID] {
|
|
success = true
|
|
mu.Unlock()
|
|
break
|
|
}
|
|
mu.Unlock()
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
if !success {
|
|
t.Error("Job was not executed in time")
|
|
}
|
|
|
|
// check that the job is removed after execution (DeleteAfterRun = true)
|
|
status := cs.Status()
|
|
if status["jobs"].(int) != 0 {
|
|
t.Errorf("Job should be deleted after run, got count: %v", status["jobs"])
|
|
}
|
|
}
|
|
|
|
func TestCronService_PersistenceIntegrity(t *testing.T) {
|
|
tmpFile := "persist_test.json"
|
|
defer os.Remove(tmpFile)
|
|
|
|
// write a job and persist
|
|
cs1 := NewCronService(tmpFile, nil)
|
|
at := int64(2000000000000)
|
|
cs1.AddJob("PersistMe", CronSchedule{Kind: "at", AtMS: &at}, "payload", true, "ch1", "")
|
|
|
|
// check file exists
|
|
if _, err := os.Stat(tmpFile); os.IsNotExist(err) {
|
|
t.Fatal("Store file was not created")
|
|
}
|
|
|
|
// reload and check data integrity
|
|
cs2 := NewCronService(tmpFile, nil)
|
|
if err := cs2.Load(); err != nil {
|
|
t.Fatalf("Failed to load store: %v", err)
|
|
}
|
|
|
|
jobs := cs2.ListJobs(true)
|
|
if len(jobs) != 1 || jobs[0].Name != "PersistMe" {
|
|
t.Errorf("Data corruption after reload. Got: %+v", jobs)
|
|
}
|
|
|
|
// test loading invalid JSON
|
|
os.WriteFile(tmpFile, []byte("{invalid json}"), 0o644)
|
|
cs3 := NewCronService(tmpFile, nil)
|
|
err := cs3.loadStore()
|
|
if err == nil {
|
|
t.Error("Should return error when loading invalid JSON")
|
|
}
|
|
}
|
|
|
|
func TestCronService_ConcurrentAccess(t *testing.T) {
|
|
cs, path := setupService(nil)
|
|
defer os.Remove(path)
|
|
|
|
cs.Start()
|
|
defer cs.Stop()
|
|
|
|
var wg sync.WaitGroup
|
|
workers := 10
|
|
iterations := 50
|
|
|
|
wg.Add(workers * 2)
|
|
|
|
// add jobs concurrently
|
|
for i := range workers {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := range iterations {
|
|
at := time.Now().Add(time.Hour).UnixMilli()
|
|
cs.AddJob(fmt.Sprintf("Job-%d-%d", id, j), CronSchedule{Kind: "at", AtMS: &at}, "", false, "", "")
|
|
time.Sleep(100 * time.Microsecond)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// read and update jobs concurrently
|
|
for range workers {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := range iterations {
|
|
jobs := cs.ListJobs(true)
|
|
if len(jobs) > 0 {
|
|
cs.EnableJob(jobs[0].ID, j%2 == 0)
|
|
}
|
|
time.Sleep(100 * time.Microsecond)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|