106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
package health
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
healthDomain "proxy-pool/internal/domain/health"
|
|
proxyDomain "proxy-pool/internal/domain/proxy"
|
|
)
|
|
|
|
func TestSchedulerRunnerUsesBoundedDueBatchAndSharedInFlightCount(t *testing.T) {
|
|
planner, err := NewPlanner(SchedulePolicy{
|
|
Interval: time.Minute, MaxInFlight: 3, Timeout: time.Second, MaxAttempts: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewPlanner(): %v", err)
|
|
}
|
|
now := time.Date(2026, 7, 31, 18, 0, 0, 0, time.UTC)
|
|
source := &dueSourceStub{inFlight: 1, candidates: []Candidate{
|
|
{ProxyID: "fetched", State: proxyDomain.StateFetched, Level: healthDomain.LevelBasic, DueAt: now},
|
|
{ProxyID: "available", State: proxyDomain.StateAvailable, Level: healthDomain.LevelBasic, DueAt: now},
|
|
}}
|
|
sink := &taskSinkStub{}
|
|
runner, err := NewSchedulerRunner(planner, source, sink, SchedulerRunnerOptions{
|
|
PollInterval: time.Second, BatchSize: 2, Now: func() time.Time { return now },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSchedulerRunner(): %v", err)
|
|
}
|
|
result, err := runner.Tick(context.Background())
|
|
if err != nil || result.Planned != 2 || result.Offered != 2 || source.limit != 2 || len(sink.tasks) != 2 ||
|
|
sink.tasks[0].Priority != PriorityFetched {
|
|
t.Fatalf("Tick() = (%+v, %v); source=%+v sink=%+v", result, err, source, sink.tasks)
|
|
}
|
|
}
|
|
|
|
func TestSchedulerRunnerSkipsDueReadAtSharedCapacity(t *testing.T) {
|
|
planner, err := NewPlanner(SchedulePolicy{
|
|
Interval: time.Minute, MaxInFlight: 1, Timeout: time.Second, MaxAttempts: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewPlanner(): %v", err)
|
|
}
|
|
source := &dueSourceStub{inFlight: 1}
|
|
runner, err := NewSchedulerRunner(planner, source, &taskSinkStub{}, SchedulerRunnerOptions{
|
|
PollInterval: time.Second, BatchSize: 1, Now: time.Now,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSchedulerRunner(): %v", err)
|
|
}
|
|
result, err := runner.Tick(context.Background())
|
|
if err != nil || result != (TickResult{}) || source.limit != 0 {
|
|
t.Fatalf("Tick(at capacity) = (%+v, %v); source=%+v", result, err, source)
|
|
}
|
|
}
|
|
|
|
func TestSchedulerRunnerRejectsOversizedSourceAndSinkResponses(t *testing.T) {
|
|
planner, err := NewPlanner(SchedulePolicy{
|
|
Interval: time.Minute, MaxInFlight: 2, Timeout: time.Second, MaxAttempts: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewPlanner(): %v", err)
|
|
}
|
|
now := time.Now()
|
|
runner, err := NewSchedulerRunner(planner, &dueSourceStub{candidates: []Candidate{
|
|
{ProxyID: "one", State: proxyDomain.StateFetched, Level: healthDomain.LevelBasic, DueAt: now},
|
|
{ProxyID: "two", State: proxyDomain.StateFetched, Level: healthDomain.LevelBasic, DueAt: now},
|
|
}}, &taskSinkStub{}, SchedulerRunnerOptions{PollInterval: time.Second, BatchSize: 1, Now: func() time.Time { return now }})
|
|
if err != nil {
|
|
t.Fatalf("NewSchedulerRunner(): %v", err)
|
|
}
|
|
if _, err := runner.Tick(context.Background()); !errors.Is(err, ErrInvalidDueSource) {
|
|
t.Fatalf("Tick(oversized source) error = %v, want ErrInvalidDueSource", err)
|
|
}
|
|
}
|
|
|
|
type dueSourceStub struct {
|
|
inFlight int
|
|
candidates []Candidate
|
|
limit int
|
|
}
|
|
|
|
func (source *dueSourceStub) InFlight(context.Context, time.Time) (int, error) {
|
|
return source.inFlight, nil
|
|
}
|
|
|
|
func (source *dueSourceStub) DueCandidates(_ context.Context, _ time.Time, limit int) ([]Candidate, error) {
|
|
source.limit = limit
|
|
return source.candidates, nil
|
|
}
|
|
|
|
type taskSinkStub struct {
|
|
tasks []PlannedTask
|
|
offer int
|
|
}
|
|
|
|
func (sink *taskSinkStub) Offer(_ context.Context, tasks []PlannedTask) (int, error) {
|
|
sink.tasks = append([]PlannedTask(nil), tasks...)
|
|
if sink.offer != 0 {
|
|
return sink.offer, nil
|
|
}
|
|
return len(tasks), nil
|
|
}
|