// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package dataplane import ( "testing" "time" ) // The grant has to be big enough that the *clock* ends a throughput run, not the byte budget. Get // this wrong and the test still "works": it stops early, reports a rate computed over a truncated // window, and nothing anywhere says the number is meaningless. So the sizing is pinned. func TestThroughputBudgetOutlastsTheRequestedRun(t *testing.T) { cases := []struct{ durationMs, kbps int }{ {1_000, 1_000}, {10_000, 50_000}, {10_000, 200_000}, {30_000, 100_000}, } for _, c := range cases { // Against the *planned* duration, which is what will actually be run: a request the // server shortens is answered with the shorter number, not truncated halfway. planMs, planKbps := ThroughputPlan(c.durationMs, c.kbps) lim := ThroughputLimits(c.durationMs, c.kbps) needed := int64(planKbps) * 1000 / 8 * int64(planMs) / 1000 if lim.MaxBytes < needed { t.Errorf("%d ms at %d kbps (planned %d ms) needs %d bytes, budget is %d - the run "+ "would stop early and report a rate over a truncated window", c.durationMs, c.kbps, planMs, needed, lim.MaxBytes) } } } // The pacer should control the rate and the grant should be the safety net. If the grant's // ceiling equals the pacing target, ordinary scheduling jitter trips it and cuts the run short // for no real reason. func TestGrantRateCeilingSitsAboveThePacingTarget(t *testing.T) { lim := ThroughputLimits(10_000, 50_000) if lim.MaxKbps <= 50_000 { t.Fatalf("grant ceiling %d kbps is not above the 50000 kbps pacing target", lim.MaxKbps) } } // A client asking for more than the server will do must get the server's number, not its own. func TestThroughputRequestsAreClamped(t *testing.T) { lim := ThroughputLimits(10*60*1000, 10_000_000) // ten minutes at 10 Gbps if lim.MaxBytes > maxThroughputBytes { t.Errorf("byte budget %d exceeds the hard cap %d", lim.MaxBytes, maxThroughputBytes) } if lim.MaxKbps > maxThroughputKbps*12/10 { t.Errorf("rate ceiling %d exceeds the hard cap", lim.MaxKbps) } // The hold has to outlast the planned run, or the grant expires mid-send and the run is // reported as rate-limited when it was really time-limited. planMs, _ := ThroughputPlan(10*60*1000, 10_000_000) if lim.MaxHold < time.Duration(planMs)*time.Millisecond { t.Errorf("hold %v is shorter than the planned run of %d ms", lim.MaxHold, planMs) } } // When the byte cap binds before the clock does, the server shortens the run and says so, rather // than accepting thirty seconds and delivering twenty-one. Same information, no surprise - and it // keeps "the clock ended the run" as the normal case, which is the only case where the resulting // rate is a clean property of the network. func TestAnOversizedRequestComesBackShorterRatherThanTruncated(t *testing.T) { const kbps = 200_000 askedMs := 30_000 planMs, planKbps := ThroughputPlan(askedMs, kbps) if planKbps != kbps { t.Errorf("rate was reduced to %d; the duration should absorb the cap, not the rate", planKbps) } if planMs >= askedMs { t.Fatalf("plan kept the full %d ms at %d kbps, which exceeds the %d byte cap", askedMs, kbps, maxThroughputBytes) } // And what it does promise must fit. if got := int64(planKbps) * 1000 / 8 * int64(planMs) / 1000; got > maxThroughputBytes { t.Errorf("planned run needs %d bytes, over the %d cap", got, maxThroughputBytes) } } // A short, ordinary request must come back untouched - the clamping only exists for the extremes. func TestAnOrdinaryRequestIsNotRewritten(t *testing.T) { planMs, planKbps := ThroughputPlan(10_000, 50_000) if planMs != 10_000 || planKbps != 50_000 { t.Errorf("10 s at 50 Mbps was rewritten to %d ms at %d kbps", planMs, planKbps) } } // Every action other than throughput stays on the small default budget. Throughput needs a big // one; raising the global default to suit it would quietly unbound everything else. func TestOnlyThroughputGetsTheLargeBudget(t *testing.T) { big := ThroughputLimits(10_000, 50_000) if big.MaxBytes <= 8<<20 { t.Fatalf("throughput budget %d is no larger than the default action budget", big.MaxBytes) } }