- internal/store/sqlite/edge_cases_test.go: 把错误的 sqlite.New 调用换成 实际存在的 sqlite.Open(ctx, dsn),清掉阻塞 `go test ./internal/...` 的 build 失败 - internal/host/sub2api/edge_cases_test.go: gofmt - internal/worker/runner_extra_test.go: TestRunnerLoggerCalled 加 sync.Mutex 保护 logger 写入的共享状态;测试结束前 cancel 并留 20ms flush 窗口,避免 -race 检测到 goroutine 仍在写 验证: gofmt -l . 干净,go vet ./... 零警告, go test -race -count=1 ./internal/... 全包通过,集成测试通过
212 lines
4.7 KiB
Go
212 lines
4.7 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRunnerNilReceiver(t *testing.T) {
|
|
// nil runner should not panic
|
|
var r *Runner
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Should not panic
|
|
r.Start(ctx)
|
|
}
|
|
|
|
func TestRunnerEmptyJobs(t *testing.T) {
|
|
runner := NewRunner([]Job{}, 10*time.Millisecond, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Should not panic with empty jobs
|
|
runner.Start(ctx)
|
|
time.Sleep(30 * time.Millisecond)
|
|
}
|
|
|
|
func TestRunnerNilJobs(t *testing.T) {
|
|
runner := NewRunner(nil, 10*time.Millisecond, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Should not panic with nil jobs
|
|
runner.Start(ctx)
|
|
time.Sleep(30 * time.Millisecond)
|
|
}
|
|
|
|
func TestRunnerWithNilJob(t *testing.T) {
|
|
runner := NewRunner([]Job{nil}, 10*time.Millisecond, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Should not panic with nil job in list
|
|
runner.Start(ctx)
|
|
time.Sleep(30 * time.Millisecond)
|
|
}
|
|
|
|
func TestRunnerContextCancellation(t *testing.T) {
|
|
job := &stubJob{}
|
|
runner := NewRunner([]Job{job}, 10*time.Millisecond, nil)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
runner.Start(ctx)
|
|
|
|
// Let it run once
|
|
time.Sleep(20 * time.Millisecond)
|
|
initialCount := job.Count()
|
|
|
|
// Cancel context
|
|
cancel()
|
|
|
|
// Wait a bit
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
// Job should not run after cancellation
|
|
if job.Count() != initialCount {
|
|
t.Errorf("job should not run after context cancellation, got %d runs", job.Count())
|
|
}
|
|
}
|
|
|
|
func TestRunnerLoggerCalled(t *testing.T) {
|
|
failingJob := &errorJob{err: errors.New("test error")}
|
|
|
|
var mu sync.Mutex
|
|
var logCalled bool
|
|
var logMsg string
|
|
logger := func(format string, args ...any) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
logCalled = true
|
|
logMsg = format
|
|
// Capture error message
|
|
if len(args) > 0 {
|
|
if s, ok := args[0].(error); ok {
|
|
logMsg = logMsg + " " + s.Error()
|
|
}
|
|
}
|
|
}
|
|
|
|
runner := NewRunner([]Job{failingJob}, 10*time.Millisecond, logger)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
runner.Start(ctx)
|
|
time.Sleep(30 * time.Millisecond)
|
|
cancel()
|
|
// Give the runner goroutine a moment to flush.
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
called := logCalled
|
|
msg := logMsg
|
|
mu.Unlock()
|
|
if !called {
|
|
t.Error("logger should be called when job fails")
|
|
}
|
|
if msg == "" {
|
|
t.Error("log message should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestRunnerNilLogger(t *testing.T) {
|
|
failingJob := &errorJob{err: errors.New("test error")}
|
|
|
|
runner := NewRunner([]Job{failingJob}, 10*time.Millisecond, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Should not panic with nil logger
|
|
runner.Start(ctx)
|
|
time.Sleep(30 * time.Millisecond)
|
|
}
|
|
|
|
func TestRunnerZeroInterval(t *testing.T) {
|
|
job := &stubJob{}
|
|
runner := NewRunner([]Job{job}, 0, nil)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
runner.Start(ctx)
|
|
|
|
// With interval=0, job should run once immediately but not on ticker
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
if job.Count() != 1 {
|
|
t.Errorf("job should run exactly once with interval=0, got %d runs", job.Count())
|
|
}
|
|
}
|
|
|
|
func TestRunnerMultipleJobs(t *testing.T) {
|
|
runner := NewRunner([]Job{&stubJob{}, &stubJob{}, &stubJob{}}, 10*time.Millisecond, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
runner.Start(ctx)
|
|
time.Sleep(25 * time.Millisecond)
|
|
|
|
// All jobs should have run
|
|
// We can't access individual counts directly, but we can verify no panic
|
|
}
|
|
|
|
func TestRunnerJobContextCancellation(t *testing.T) {
|
|
runner := NewRunner([]Job{&slowJob{duration: 100 * time.Millisecond}}, 50*time.Millisecond, nil)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
runner.Start(ctx)
|
|
|
|
// Let it start
|
|
time.Sleep(10 * time.Millisecond)
|
|
// Cancel while job might be running
|
|
cancel()
|
|
|
|
// Should not panic or deadlock
|
|
time.Sleep(150 * time.Millisecond)
|
|
}
|
|
|
|
func TestRunnerJobsIsolation(t *testing.T) {
|
|
runner := NewRunner([]Job{&errorJob{err: errors.New("fail")}, nil, &stubJob{}}, 10*time.Millisecond, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
runner.Start(ctx)
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
// Should not panic with mixed jobs
|
|
}
|
|
|
|
// Helper types
|
|
|
|
type errorJob struct {
|
|
err error
|
|
}
|
|
|
|
func (j *errorJob) Name() string {
|
|
return "errorJob"
|
|
}
|
|
|
|
func (j *errorJob) Run(context.Context) error {
|
|
return j.err
|
|
}
|
|
|
|
type slowJob struct {
|
|
duration time.Duration
|
|
}
|
|
|
|
func (j *slowJob) Name() string {
|
|
return "slow"
|
|
}
|
|
|
|
func (j *slowJob) Run(ctx context.Context) error {
|
|
select {
|
|
case <-time.After(j.duration):
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|