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