Files
sub2api-cn-relay-manager/internal/worker/runner_extra_test.go
phamnazage-jpg 4ec9dad44f
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled
test: 修 build-broken edge-case 测试
- 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/... 全包通过,集成测试通过
2026-06-02 20:38:29 +08:00

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()
}
}