You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
267 lines
5.7 KiB
267 lines
5.7 KiB
package graph |
|
|
|
import ( |
|
"context" |
|
"testing" |
|
"time" |
|
) |
|
|
|
func TestRateLimiterQueryCost(t *testing.T) { |
|
rl := NewRateLimiter(DefaultRateLimiterConfig()) |
|
|
|
tests := []struct { |
|
name string |
|
query *Query |
|
minCost float64 |
|
maxCost float64 |
|
}{ |
|
{ |
|
name: "nil query", |
|
query: nil, |
|
minCost: 1.0, |
|
maxCost: 1.0, |
|
}, |
|
{ |
|
name: "depth 1 no refs", |
|
query: &Query{ |
|
Method: "follows", |
|
Seed: "abc", |
|
Depth: 1, |
|
}, |
|
minCost: 1.5, // depthFactor^1 = 2 |
|
maxCost: 2.5, |
|
}, |
|
{ |
|
name: "depth 2 no refs", |
|
query: &Query{ |
|
Method: "follows", |
|
Seed: "abc", |
|
Depth: 2, |
|
}, |
|
minCost: 3.5, // depthFactor^2 = 4 |
|
maxCost: 4.5, |
|
}, |
|
{ |
|
name: "depth 3 no refs", |
|
query: &Query{ |
|
Method: "follows", |
|
Seed: "abc", |
|
Depth: 3, |
|
}, |
|
minCost: 7.5, // depthFactor^3 = 8 |
|
maxCost: 8.5, |
|
}, |
|
{ |
|
name: "depth 2 with inbound refs", |
|
query: &Query{ |
|
Method: "follows", |
|
Seed: "abc", |
|
Depth: 2, |
|
InboundRefs: []RefSpec{ |
|
{Kinds: []int{7}}, |
|
}, |
|
}, |
|
minCost: 4.0, // 4 + 0.5 = 4.5 |
|
maxCost: 5.0, |
|
}, |
|
{ |
|
name: "depth 2 with both refs", |
|
query: &Query{ |
|
Method: "follows", |
|
Seed: "abc", |
|
Depth: 2, |
|
InboundRefs: []RefSpec{ |
|
{Kinds: []int{7}}, |
|
}, |
|
OutboundRefs: []RefSpec{ |
|
{Kinds: []int{1}}, |
|
}, |
|
}, |
|
minCost: 4.5, // 4 + 0.5 + 0.5 = 5 |
|
maxCost: 5.5, |
|
}, |
|
} |
|
|
|
for _, tt := range tests { |
|
t.Run(tt.name, func(t *testing.T) { |
|
cost := rl.QueryCost(tt.query) |
|
if cost < tt.minCost || cost > tt.maxCost { |
|
t.Errorf("QueryCost() = %v, want between %v and %v", cost, tt.minCost, tt.maxCost) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
func TestRateLimiterOperationCost(t *testing.T) { |
|
rl := NewRateLimiter(DefaultRateLimiterConfig()) |
|
|
|
// Depth 0, 1 node |
|
cost0 := rl.OperationCost(0, 1) |
|
if cost0 < 1.0 || cost0 > 1.1 { |
|
t.Errorf("OperationCost(0, 1) = %v, want ~1.01", cost0) |
|
} |
|
|
|
// Depth 1, 1 node |
|
cost1 := rl.OperationCost(1, 1) |
|
if cost1 < 2.0 || cost1 > 2.1 { |
|
t.Errorf("OperationCost(1, 1) = %v, want ~2.02", cost1) |
|
} |
|
|
|
// Depth 2, 100 nodes |
|
cost2 := rl.OperationCost(2, 100) |
|
if cost2 < 8.0 { |
|
t.Errorf("OperationCost(2, 100) = %v, want > 8", cost2) |
|
} |
|
} |
|
|
|
func TestRateLimiterAcquire(t *testing.T) { |
|
cfg := DefaultRateLimiterConfig() |
|
cfg.MaxTokens = 10 |
|
cfg.RefillRate = 100 // Fast refill for testing |
|
rl := NewRateLimiter(cfg) |
|
|
|
ctx := context.Background() |
|
|
|
// Should acquire immediately when tokens available |
|
delay, err := rl.Acquire(ctx, 5) |
|
if err != nil { |
|
t.Fatalf("unexpected error: %v", err) |
|
} |
|
if delay > time.Millisecond*10 { |
|
t.Errorf("expected minimal delay, got %v", delay) |
|
} |
|
|
|
// Check remaining tokens |
|
remaining := rl.AvailableTokens() |
|
if remaining > 6 { |
|
t.Errorf("expected ~5 tokens remaining, got %v", remaining) |
|
} |
|
} |
|
|
|
func TestRateLimiterTryAcquire(t *testing.T) { |
|
cfg := DefaultRateLimiterConfig() |
|
cfg.MaxTokens = 10 |
|
rl := NewRateLimiter(cfg) |
|
|
|
// Should succeed with enough tokens |
|
if !rl.TryAcquire(5) { |
|
t.Error("TryAcquire(5) should succeed with 10 tokens") |
|
} |
|
|
|
// Should succeed again |
|
if !rl.TryAcquire(5) { |
|
t.Error("TryAcquire(5) should succeed with 5 tokens") |
|
} |
|
|
|
// Should fail with insufficient tokens |
|
if rl.TryAcquire(1) { |
|
t.Error("TryAcquire(1) should fail with 0 tokens") |
|
} |
|
} |
|
|
|
func TestRateLimiterContextCancellation(t *testing.T) { |
|
cfg := DefaultRateLimiterConfig() |
|
cfg.MaxTokens = 1 |
|
cfg.RefillRate = 0.1 // Very slow refill |
|
rl := NewRateLimiter(cfg) |
|
|
|
// Drain tokens |
|
rl.TryAcquire(1) |
|
|
|
// Create cancellable context |
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) |
|
defer cancel() |
|
|
|
// Try to acquire - should be cancelled |
|
_, err := rl.Acquire(ctx, 10) |
|
if err != context.DeadlineExceeded { |
|
t.Errorf("expected DeadlineExceeded, got %v", err) |
|
} |
|
} |
|
|
|
func TestRateLimiterRefill(t *testing.T) { |
|
cfg := DefaultRateLimiterConfig() |
|
cfg.MaxTokens = 10 |
|
cfg.RefillRate = 1000 // 1000 tokens per second |
|
rl := NewRateLimiter(cfg) |
|
|
|
// Drain tokens |
|
rl.TryAcquire(10) |
|
|
|
// Wait for refill |
|
time.Sleep(15 * time.Millisecond) |
|
|
|
// Should have some tokens now |
|
available := rl.AvailableTokens() |
|
if available < 5 { |
|
t.Errorf("expected >= 5 tokens after 15ms at 1000/s, got %v", available) |
|
} |
|
if available > 10 { |
|
t.Errorf("expected <= 10 tokens (max), got %v", available) |
|
} |
|
} |
|
|
|
func TestRateLimiterPause(t *testing.T) { |
|
rl := NewRateLimiter(DefaultRateLimiterConfig()) |
|
ctx := context.Background() |
|
|
|
start := time.Now() |
|
err := rl.Pause(ctx, 1, 0) |
|
elapsed := time.Since(start) |
|
|
|
if err != nil { |
|
t.Fatalf("unexpected error: %v", err) |
|
} |
|
|
|
// Should have paused for at least baseDelay |
|
if elapsed < rl.baseDelay { |
|
t.Errorf("pause duration %v < baseDelay %v", elapsed, rl.baseDelay) |
|
} |
|
} |
|
|
|
func TestThrottler(t *testing.T) { |
|
cfg := DefaultRateLimiterConfig() |
|
cfg.BaseDelay = 100 * time.Microsecond // Short for testing |
|
rl := NewRateLimiter(cfg) |
|
|
|
throttler := NewThrottler(rl, 1) |
|
ctx := context.Background() |
|
|
|
// Process items |
|
for i := 0; i < 100; i++ { |
|
if err := throttler.Tick(ctx); err != nil { |
|
t.Fatalf("unexpected error at tick %d: %v", i, err) |
|
} |
|
} |
|
|
|
processed := throttler.Complete() |
|
if processed != 100 { |
|
t.Errorf("expected 100 items processed, got %d", processed) |
|
} |
|
} |
|
|
|
func TestThrottlerContextCancellation(t *testing.T) { |
|
cfg := DefaultRateLimiterConfig() |
|
rl := NewRateLimiter(cfg) |
|
|
|
throttler := NewThrottler(rl, 2) // depth 2 = more frequent pauses |
|
ctx, cancel := context.WithCancel(context.Background()) |
|
|
|
// Process some items |
|
for i := 0; i < 20; i++ { |
|
throttler.Tick(ctx) |
|
} |
|
|
|
// Cancel context |
|
cancel() |
|
|
|
// Next tick that would pause should return error |
|
for i := 0; i < 100; i++ { |
|
if err := throttler.Tick(ctx); err != nil { |
|
// Expected - context was cancelled |
|
return |
|
} |
|
} |
|
// If we get here without error, the throttler didn't check context |
|
// This is acceptable if no pause was needed |
|
}
|
|
|