Files
Mitchell-JohnsonandClaude Opus 5 48d3887514 A second download source, for when the first one refuses
Cloudflare rate-limits by volume, and the block it handed out while this was
being built lasted over twenty minutes — too fragile to be the only thing behind
a button. So the download phase tries its sources in order: the anycast endpoint
first, then a plain 100 MB file on Linode's Seattle mirror, which has no opinion
about how much has been pulled today.

Only a refusal moves to the next one; anything else would fail the same way
twice. Every result names the source it used, and the page says "from
linode-seattle" rather than "via SEA" when it was the fallback — that mirror is
in one fixed place, so somewhere else it reads low, and a figure that quietly
came from a different continent would be a lie.

Upload stays on Cloudflare, which has never been the one to refuse.

Also: the too-short-window guard was refusing to divide 512 MiB by ten
milliseconds on a link fast enough to hit the runaway budget before the first
tick. It now guards against dividing by zero and nothing more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 20:15:33 -07:00

318 lines
10 KiB
Go

package network
import (
"context"
"errors"
"io"
"math"
"net/http"
"net/http/httptest"
"os"
"sync/atomic"
"testing"
"time"
)
func TestHexIP(t *testing.T) {
// /proc/net/route prints each address as a host-order word: on a little-endian machine
// 10.0.0.1 is written "0100000A". These cases are that machine, which is all of ours.
for hex, want := range map[string]string{
"0100000A": "10.0.0.1",
"00000000": "0.0.0.0",
"0101A8C0": "192.168.1.1",
} {
got, err := hexIP(hex)
if err != nil || got != want {
t.Errorf("hexIP(%q) = %q, %v; want %q", hex, got, err, want)
}
}
if _, err := hexIP("nonsense"); err == nil {
t.Error("hexIP accepted nonsense")
}
}
func TestServerTimeIsNotLatency(t *testing.T) {
h := http.Header{}
h.Add("Server-Timing", "cfSpeedEdge;dur=7, cfSpeedWorker;dur=36")
h.Add("Server-Timing", `cfL4;desc="?proto=TCP&rtt=3505&min_rtt=2856"`)
if got := serverMs(h); got != 43 {
t.Errorf("serverMs = %v, want 43 (7+36, and nothing from the cfL4 description)", got)
}
if got := serverMs(http.Header{}); got != 0 {
t.Errorf("serverMs with no header = %v, want 0", got)
}
h2 := http.Header{"Cf-Ray": {"a41f6720f966a36c-SEA"}, "Cf-Meta-Ip": {"50.47.252.221"}}
if m := readMeta(h2); m.Colo != "SEA" || m.IP != "50.47.252.221" {
t.Errorf("readMeta = %+v, want the edge SEA and the address", m)
}
}
func TestRates(t *testing.T) {
// a gibibyte in eight seconds is 1073.7 Mbps
if got := mbps(1<<30, 8); math.Abs(got-1073.7) > 0.1 {
t.Errorf("mbps(1GiB, 8s) = %v, want ~1073.7", got)
}
if got := best([]float64{12.5, 9.2, 40, 9.9}); got != 9.2 {
t.Errorf("best = %v, want 9.2 (the least of them)", got)
}
if got := jitter([]float64{10, 10, 10}); got != 0 {
t.Errorf("jitter of a steady line = %v, want 0", got)
}
if got := jitter([]float64{10}); got != 0 {
t.Errorf("jitter of one sample = %v, want 0", got)
}
if got := jitter([]float64{5, 50, 5, 50}); got != 45 {
t.Errorf("jitter of a line alternating by 45 = %v, want 45", got)
}
// one slow probe among steady ones moves it a little, not enormously
if got := jitter([]float64{10, 10, 10, 60, 10, 10}); got > 25 {
t.Errorf("jitter with a single outlier = %v, want it not to run away", got)
}
}
// The body an upload sends is a fixed size, counts itself, and costs nothing to make.
func TestZerosBodyCountsItself(t *testing.T) {
var n atomic.Int64
z := &zeros{left: 100, n: &n}
buf := make([]byte, 30)
total := 0
for {
got, err := z.Read(buf)
total += got
if err != nil {
break
}
}
if total != 100 || n.Load() != 100 {
t.Errorf("read %d bytes and counted %d, want 100 and 100", total, n.Load())
}
}
// Interfaces has to work on whatever machine this runs on, and always name a loopback.
func TestInterfacesHasLoopback(t *testing.T) {
ifs := Interfaces()
if len(ifs) == 0 {
t.Fatal("no interfaces at all")
}
lo := false
for _, f := range ifs {
if f.Kind == "loopback" {
lo = true
if !f.Up || len(f.Addrs) == 0 {
t.Errorf("loopback %s looks wrong: up=%v addrs=%v", f.Name, f.Up, f.Addrs)
}
}
if f.Name == "" {
t.Error("an interface with no name")
}
}
if !lo {
t.Error("no loopback among the interfaces")
}
}
// The real thing, against the real endpoint: skipped by default, because `make test` must
// not need the internet. HAMLAB_NET_TEST=1 go test ./internal/network/ -run Live -v
func TestLiveSpeedTest(t *testing.T) {
if os.Getenv("HAMLAB_NET_TEST") == "" {
t.Skip("set HAMLAB_NET_TEST=1 to measure the actual line")
}
tr := NewTester(nil)
if _, err := tr.Start(); err != nil {
t.Fatal(err)
}
if _, err := tr.Start(); err == nil {
t.Error("a second test started while the first was running")
}
deadline := time.Now().Add(90 * time.Second)
for tr.Running() && time.Now().Before(deadline) {
time.Sleep(250 * time.Millisecond)
}
st := tr.State()
if st.Result == nil {
t.Fatal("no result")
}
r := *st.Result
t.Logf("down %s up %s latency %.1f ms jitter %.1f ms via %s ip %s %.0f MB moved in %.1fs",
Mbits(r.DownMbps), Mbits(r.UpMbps), r.LatencyMs, r.JitterMs, r.Colo, r.IP,
float64(r.DownBytes+r.UpBytes)/1e6, r.TookSec)
if r.Error != "" {
t.Fatalf("test failed: %s", r.Error)
}
if r.DownMbps <= 0 || r.UpMbps <= 0 || r.LatencyMs <= 0 {
t.Errorf("a figure came back zero: %+v", r)
}
if r.IP == "" {
t.Error("no public address reported")
}
if len(tr.History()) != 1 {
t.Errorf("history has %d results, want 1", len(tr.History()))
}
}
// A phase measured against a local server, at a rate the test controls. This is the part of
// the speed test with arithmetic in it — the window after the ramp, the byte cap that ends it
// early, and the rate that comes out — and none of it needs the internet to check.
func TestPhaseMeasuresTheWindow(t *testing.T) {
const perTick = 1 << 20 // a mebibyte every 5 ms ≈ 1678 Mbps, whatever the machine is doing
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/up" {
_, _ = io.Copy(io.Discard, r.Body)
w.WriteHeader(204)
return
}
buf := make([]byte, perTick)
fl, _ := w.(http.Flusher)
for {
if _, err := w.Write(buf); err != nil {
return
}
if fl != nil {
fl.Flush()
}
select {
case <-time.After(5 * time.Millisecond):
case <-r.Context().Done():
return
}
}
}))
defer srv.Close()
defer func(up string, w, d, mw time.Duration, m int64) {
upURL, warmUp, downFor, minWindow, measureBytes = up, w, d, mw, m
}(upURL, warmUp, downFor, minWindow, measureBytes)
upURL = srv.URL + "/up"
warmUp, downFor, minWindow, measureBytes = 100*time.Millisecond, 3*time.Second, 200*time.Millisecond, 8<<20
here := Source{"local", func(int) string { return srv.URL + "/down" }}
tr := NewTester(nil)
began := time.Now()
mbps, moved, note, err := tr.phase(context.Background(), srv.Client(), "download", 2, downFor, 0, 1, here)
took := time.Since(began)
if err != nil {
t.Fatalf("phase: %v", err)
}
if note != "" {
t.Errorf("note = %q, want none — nothing went wrong", note)
}
// The cap must have ended it early rather than the clock: the phase was allowed three
// seconds and should be done in well under one, having moved at least the cap.
if moved < measureBytes {
t.Errorf("moved %d bytes, want at least the %d-byte cap", moved, measureBytes)
}
if took > time.Second {
t.Errorf("phase took %v of the %v it was allowed; the byte cap should have ended it", took.Round(time.Millisecond), downFor)
}
// The server hands out a mebibyte per 5 ms per stream, so the rate is in the hundreds of
// Mbps. The point is that it is a real quotient of what moved over how long, not that it
// hits a particular figure on a loaded test machine.
if mbps < 50 || mbps > 20000 {
t.Errorf("mbps = %v, which is not a believable quotient", mbps)
}
t.Logf("measured %.0f Mbps over %.1f MiB", mbps, float64(moved)/(1<<20))
// A phase whose server refuses outright fails, and says what it was told.
bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(429) }))
defer bad.Close()
refuser := Source{"refuser", func(int) string { return bad.URL + "/down" }}
_, _, _, err = tr.phase(context.Background(), bad.Client(), "download", 2, 300*time.Millisecond, 0, 1, refuser)
if err == nil {
t.Error("a phase against a server that refuses everything should fail")
} else if !refused(err) {
t.Errorf("error = %q, want the refusal that makes the fallback kick in", err)
}
}
// Stopping a test mid-flight is reported as cancelled, not as whatever the streams said on
// their way out.
func TestCancelIsReportedAsCancelled(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf := make([]byte, 1<<16)
for {
if _, err := w.Write(buf); err != nil {
return
}
select {
case <-time.After(time.Millisecond):
case <-r.Context().Done():
return
}
}
}))
defer srv.Close()
defer func(w, d time.Duration) { warmUp, downFor = w, d }(warmUp, downFor)
warmUp, downFor = 10*time.Millisecond, 10*time.Second
here := Source{"local", func(int) string { return srv.URL + "/down" }}
tr := NewTester(nil)
ctx, cancel := context.WithCancel(context.Background())
go func() { time.Sleep(200 * time.Millisecond); cancel() }()
_, _, _, err := tr.phase(ctx, srv.Client(), "download", 1, downFor, 0, 1, here)
// Cancelled after bytes moved: the figure stands, and the phase does not claim an error.
if err != nil && !errors.Is(err, context.Canceled) {
t.Errorf("err = %v, want nil or a cancellation", err)
}
}
// When the first source refuses, the download is taken from the next one and the result says
// so. This is the whole reason there is more than one.
func TestDownloadFallsBackWhenRefused(t *testing.T) {
serving := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/up" {
_, _ = io.Copy(io.Discard, r.Body)
w.WriteHeader(204)
return
}
// paced, so the phase is governed by its window and not by a loopback moving a
// gibibyte before the first tick
buf := make([]byte, 1<<20)
fl, _ := w.(http.Flusher)
for i := 0; i < 64; i++ {
if _, err := w.Write(buf); err != nil {
return
}
if fl != nil {
fl.Flush()
}
select {
case <-time.After(2 * time.Millisecond):
case <-r.Context().Done():
return
}
}
}))
defer serving.Close()
refusing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(429) }))
defer refusing.Close()
defer func(srcs []Source, up string, w, d, mw time.Duration, m int64) {
downSources, upURL, warmUp, downFor, minWindow, measureBytes = srcs, up, w, d, mw, m
}(downSources, upURL, warmUp, downFor, minWindow, measureBytes)
downSources = []Source{
{"refuser", func(int) string { return refusing.URL + "/down" }},
{"the-other-one", func(int) string { return serving.URL + "/down" }},
}
upURL = serving.URL + "/up"
warmUp, downFor, upFor, minWindow, measureBytes = 50*time.Millisecond, time.Second, 300*time.Millisecond, 100*time.Millisecond, 4<<20
tr := NewTester(nil)
if _, err := tr.Start(); err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(30 * time.Second)
for tr.Running() && time.Now().Before(deadline) {
time.Sleep(50 * time.Millisecond)
}
res := tr.State().Result
if res == nil {
t.Fatal("no result")
}
if res.Source != "the-other-one" {
t.Errorf("source = %q, want the second one — the first refused", res.Source)
}
if res.DownMbps <= 0 {
t.Errorf("download = %v, want a figure from the source that did serve: %+v", res.DownMbps, res)
}
}