1package quicnettest
2
3import (
4 "encoding/binary"
5 "net"
6 "reflect"
7 "sync"
8 "testing"
9 "time"
10)
11
12const awaitTimeout = 3 * time.Second
13
14// scriptedRNG returns pre-programmed values so tests can force impairment
15// decisions without depending on a seed's random stream.
16type scriptedRNG struct {
17 mu sync.Mutex
18 values []int
19 next int
20}
21
22func (s *scriptedRNG) Intn(n int) int {
23 s.mu.Lock()
24 defer s.mu.Unlock()
25 if len(s.values) == 0 || n <= 0 {
26 return 0
27 }
28 value := s.values[s.next%len(s.values)]
29 s.next++
30 return value % n
31}
32
33type datagram struct {
34 payload []byte
35 from *net.UDPAddr
36}
37
38func startSinkServer(t *testing.T) (*net.UDPAddr, <-chan datagram) {
39 t.Helper()
40 conn, packets := serveUDP(t, false)
41 return conn.LocalAddr().(*net.UDPAddr), packets
42}
43
44func startEchoServer(t *testing.T) (*net.UDPAddr, <-chan datagram) {
45 t.Helper()
46 conn, packets := serveUDP(t, true)
47 return conn.LocalAddr().(*net.UDPAddr), packets
48}
49
50func serveUDP(t *testing.T, echo bool) (*net.UDPConn, <-chan datagram) {
51 t.Helper()
52 conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
53 if err != nil {
54 t.Fatalf("listen udp server: %v", err)
55 }
56 t.Cleanup(func() { _ = conn.Close() })
57 // Flows burst hundreds of MTU-sized datagrams. The default receive buffer
58 // (about 200 KiB) holds too few of them when a loaded machine delays the
59 // reader, and the kernel then drops datagrams the test is still counting.
60 if err := conn.SetReadBuffer(4 << 20); err != nil {
61 t.Fatalf("size udp server buffer: %v", err)
62 }
63 packets := make(chan datagram, 4096)
64 go func() {
65 buf := make([]byte, 2048)
66 for {
67 n, from, err := conn.ReadFromUDP(buf)
68 if err != nil {
69 return
70 }
71 payload := append([]byte(nil), buf[:n]...)
72 packets <- datagram{payload: payload, from: from}
73 if echo {
74 if _, err := conn.WriteToUDP(payload, from); err != nil {
75 return
76 }
77 }
78 }
79 }()
80 return conn, packets
81}
82
83func newUDPClient(t *testing.T) *net.UDPConn {
84 t.Helper()
85 conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
86 if err != nil {
87 t.Fatalf("listen udp client: %v", err)
88 }
89 t.Cleanup(func() { _ = conn.Close() })
90 return conn
91}
92
93func newTestProxy(t *testing.T, cfg Config) (*Proxy, *ManualClock, <-chan Event) {
94 t.Helper()
95 clock := NewManualClock(time.Unix(1000, 0))
96 events := make(chan Event, 4096)
97 cfg.Clock = clock
98 cfg.OnPacket = func(event Event) { events <- event }
99 proxy, err := New(cfg)
100 if err != nil {
101 t.Fatalf("new proxy: %v", err)
102 }
103 t.Cleanup(func() { _ = proxy.Close() })
104 return proxy, clock, events
105}
106
107func send(t *testing.T, conn *net.UDPConn, dst *net.UDPAddr, payload []byte) {
108 t.Helper()
109 if _, err := conn.WriteToUDP(payload, dst); err != nil {
110 t.Fatalf("send %d-byte packet: %v", len(payload), err)
111 }
112}
113
114// sendPackets sends count datagrams in bounded bursts and returns the events
115// observed for them, so the client never outruns the proxy's receive buffer.
116func sendPackets(t *testing.T, client *net.UDPConn, dst *net.UDPAddr, events <-chan Event, count, size int) []Event {
117 t.Helper()
118 const burst = 20
119 payload := make([]byte, size)
120 observed := make([]Event, 0, count)
121 for sent := 0; sent < count; {
122 stop := sent + burst
123 if stop > count {
124 stop = count
125 }
126 for i := sent; i < stop; i++ {
127 binary.BigEndian.PutUint32(payload, uint32(i))
128 send(t, client, dst, payload)
129 }
130 for i := sent; i < stop; i++ {
131 observed = append(observed, awaitEvent(t, events))
132 }
133 sent = stop
134 }
135 return observed
136}
137
138func readPacket(t *testing.T, conn *net.UDPConn) []byte {
139 t.Helper()
140 if err := conn.SetReadDeadline(time.Now().Add(awaitTimeout)); err != nil {
141 t.Fatalf("set read deadline: %v", err)
142 }
143 buf := make([]byte, 2048)
144 n, _, err := conn.ReadFromUDP(buf)
145 if err != nil {
146 t.Fatalf("read reply: %v", err)
147 }
148 return append([]byte(nil), buf[:n]...)
149}
150
151func awaitPacket(t *testing.T, packets <-chan datagram) datagram {
152 t.Helper()
153 select {
154 case packet := <-packets:
155 return packet
156 case <-time.After(awaitTimeout):
157 t.Fatal("timed out waiting for a server receipt")
158 }
159 return datagram{}
160}
161
162func awaitEvent(t *testing.T, events <-chan Event) Event {
163 t.Helper()
164 select {
165 case event := <-events:
166 return event
167 case <-time.After(awaitTimeout):
168 t.Fatal("timed out waiting for an impairment event")
169 }
170 return Event{}
171}
172
173func expectNoDatagram(t *testing.T, packets <-chan datagram) {
174 t.Helper()
175 select {
176 case packet := <-packets:
177 t.Fatalf("unexpected server receipt %q", packet.payload)
178 default:
179 }
180}
181
182func TestNewRejectsInvalidConfig(t *testing.T) {
183 if _, err := New(Config{}); err == nil {
184 t.Fatal("New accepted a config without ServerAddr")
185 }
186 server := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 9}
187 if _, err := New(Config{ServerAddr: server, ToServer: LinkConfig{LossPercent: 101}}); err == nil {
188 t.Fatal("New accepted LossPercent 101")
189 }
190 if _, err := New(Config{ServerAddr: server, ToClient: LinkConfig{Blackouts: []Blackout{{Start: time.Second, End: time.Second}}}}); err == nil {
191 t.Fatal("New accepted an empty blackout window")
192 }
193}
194
195func TestProxyForwardsTrafficBothDirections(t *testing.T) {
196 serverAddr, serverPackets := startEchoServer(t)
197 proxy, _, events := newTestProxy(t, Config{ServerAddr: serverAddr, Seed: 1})
198 client := newUDPClient(t)
199
200 send(t, client, proxy.Addr(), []byte("hello quic"))
201
202 up := awaitEvent(t, events)
203 if up.Direction != ToServer {
204 t.Fatalf("first event direction = %s, want to-server", up.Direction)
205 }
206 if up.Decision.Drop {
207 t.Fatalf("to-server decision = %+v, want forward", up.Decision)
208 }
209 if got := string(awaitPacket(t, serverPackets).payload); got != "hello quic" {
210 t.Fatalf("server received %q, want %q", got, "hello quic")
211 }
212
213 down := awaitEvent(t, events)
214 if down.Direction != ToClient {
215 t.Fatalf("second event direction = %s, want to-client", down.Direction)
216 }
217 if got := string(readPacket(t, client)); got != "hello quic" {
218 t.Fatalf("client received %q, want %q", got, "hello quic")
219 }
220
221 stats := proxy.Stats()
222 if stats.ToServer.Received != 1 || stats.ToServer.Sent != 1 {
223 t.Fatalf("to-server stats = %s, want one received and sent packet", stats.ToServer)
224 }
225 if stats.ToClient.Received != 1 || stats.ToClient.Sent != 1 {
226 t.Fatalf("to-client stats = %s, want one received and sent packet", stats.ToClient)
227 }
228 if stats.StrayPackets != 0 || stats.Rebinds != 0 {
229 t.Fatalf("unexpected stray/rebind counters: %s", stats)
230 }
231}
232
233func TestProxySeededLossIsReproducible(t *testing.T) {
234 const packets = 100
235 run := func(t *testing.T) (Stats, []Event) {
236 t.Helper()
237 serverAddr, serverPackets := startSinkServer(t)
238 proxy, clock, events := newTestProxy(t, Config{
239 ServerAddr: serverAddr,
240 Seed: 42,
241 ToServer: LinkConfig{BaseLatency: time.Millisecond, LossPercent: 25},
242 })
243 client := newUDPClient(t)
244 observed := sendPackets(t, client, proxy.Addr(), events, packets, 64)
245
246 expected := 0
247 for _, event := range observed {
248 if !event.Decision.Drop {
249 expected++
250 }
251 }
252 clock.Advance(time.Millisecond)
253 for i := 0; i < expected; i++ {
254 awaitPacket(t, serverPackets)
255 }
256 stats := proxy.Stats()
257 if stats.ToServer.Received != packets {
258 t.Fatalf("received = %d, want %d", stats.ToServer.Received, packets)
259 }
260 return stats, observed
261 }
262
263 firstStats, firstEvents := run(t)
264 secondStats, secondEvents := run(t)
265 if firstStats.ToServer != secondStats.ToServer {
266 t.Fatalf("same seed produced different stats:\nfirst = %s\nsecond = %s", firstStats.ToServer, secondStats.ToServer)
267 }
268 if !reflect.DeepEqual(firstEvents, secondEvents) {
269 t.Fatal("same seed produced different impairment decisions")
270 }
271 if firstStats.ToServer.LossDrops == 0 || firstStats.ToServer.LossDrops >= packets {
272 t.Fatalf("loss drops = %d, want a partial loss sample", firstStats.ToServer.LossDrops)
273 }
274 if want := uint64(packets) - firstStats.ToServer.LossDrops; firstStats.ToServer.Sent != want {
275 t.Fatalf("sent = %d, want %d", firstStats.ToServer.Sent, want)
276 }
277}
278
279func TestProxyDuplicationRepeatsPackets(t *testing.T) {
280 const packets = 3
281 serverAddr, serverPackets := startSinkServer(t)
282 proxy, _, events := newTestProxy(t, Config{
283 ServerAddr: serverAddr,
284 Seed: 3,
285 ToServer: LinkConfig{DuplicatePercent: 100},
286 })
287 client := newUDPClient(t)
288
289 observed := sendPackets(t, client, proxy.Addr(), events, packets, 32)
290 for i, event := range observed {
291 if event.Decision.Duplicates != 1 {
292 t.Fatalf("packet %d decision = %+v, want one duplicate", i, event.Decision)
293 }
294 }
295 for i := 0; i < packets*2; i++ {
296 awaitPacket(t, serverPackets)
297 }
298 stats := proxy.Stats()
299 if stats.ToServer.Duplicates != packets {
300 t.Fatalf("duplicates = %d, want %d", stats.ToServer.Duplicates, packets)
301 }
302 if stats.ToServer.Sent != packets*2 {
303 t.Fatalf("sent = %d, want %d", stats.ToServer.Sent, packets*2)
304 }
305}
306
307func TestProxyReorderingDeliversOutOfOrder(t *testing.T) {
308 serverAddr, serverPackets := startSinkServer(t)
309 proxy, clock, events := newTestProxy(t, Config{
310 ServerAddr: serverAddr,
311 RNG: &scriptedRNG{values: []int{0, 99}},
312 ToServer: LinkConfig{ReorderPercent: 50, ReorderDelay: 5 * time.Millisecond},
313 })
314 client := newUDPClient(t)
315
316 send(t, client, proxy.Addr(), []byte("first"))
317 send(t, client, proxy.Addr(), []byte("second"))
318
319 first := awaitEvent(t, events)
320 if !first.Decision.Reordered {
321 t.Fatalf("first decision = %+v, want reordered", first.Decision)
322 }
323 second := awaitEvent(t, events)
324 if second.Decision.Reordered {
325 t.Fatalf("second decision = %+v, want not reordered", second.Decision)
326 }
327 if !second.Due.Before(first.Due) {
328 t.Fatalf("second due %s is not before first due %s", second.Due, first.Due)
329 }
330
331 clock.Advance(5 * time.Millisecond)
332 if got := string(awaitPacket(t, serverPackets).payload); got != "second" {
333 t.Fatalf("first server receipt = %q, want %q", got, "second")
334 }
335 if got := string(awaitPacket(t, serverPackets).payload); got != "first" {
336 t.Fatalf("second server receipt = %q, want %q", got, "first")
337 }
338 if stats := proxy.Stats(); stats.ToServer.Reordered != 1 {
339 t.Fatalf("reordered = %d, want 1", stats.ToServer.Reordered)
340 }
341}
342
343func TestProxyLatencyHoldsPacketsUntilClockAdvances(t *testing.T) {
344 serverAddr, serverPackets := startSinkServer(t)
345 proxy, clock, events := newTestProxy(t, Config{
346 ServerAddr: serverAddr,
347 Seed: 4,
348 ToServer: LinkConfig{BaseLatency: 3 * time.Millisecond},
349 })
350 client := newUDPClient(t)
351
352 send(t, client, proxy.Addr(), []byte("held"))
353 awaitEvent(t, events)
354 expectNoDatagram(t, serverPackets)
355
356 clock.Advance(2 * time.Millisecond)
357 expectNoDatagram(t, serverPackets)
358
359 clock.Advance(time.Millisecond)
360 if got := string(awaitPacket(t, serverPackets).payload); got != "held" {
361 t.Fatalf("server received %q, want %q", got, "held")
362 }
363 if stats := proxy.Stats(); stats.ToServer.Sent != 1 {
364 t.Fatalf("sent = %d, want 1", stats.ToServer.Sent)
365 }
366}
367
368func TestProxyBlackoutWindowDropsDuringInterval(t *testing.T) {
369 serverAddr, serverPackets := startSinkServer(t)
370 proxy, clock, events := newTestProxy(t, Config{
371 ServerAddr: serverAddr,
372 Seed: 5,
373 ToServer: LinkConfig{Blackouts: []Blackout{{
374 Start: 5 * time.Millisecond,
375 End: 10 * time.Millisecond,
376 }}},
377 })
378 client := newUDPClient(t)
379
380 send(t, client, proxy.Addr(), []byte("before"))
381 if event := awaitEvent(t, events); event.Decision.Drop {
382 t.Fatalf("packet before blackout dropped: %+v", event.Decision)
383 }
384 if got := string(awaitPacket(t, serverPackets).payload); got != "before" {
385 t.Fatalf("server received %q, want %q", got, "before")
386 }
387
388 clock.Advance(5 * time.Millisecond)
389 send(t, client, proxy.Addr(), []byte("during"))
390 during := awaitEvent(t, events)
391 if !during.Decision.Drop || !during.Decision.Blackout {
392 t.Fatalf("during decision = %+v, want a blackout drop", during.Decision)
393 }
394
395 clock.Advance(6 * time.Millisecond)
396 send(t, client, proxy.Addr(), []byte("after"))
397 if event := awaitEvent(t, events); event.Decision.Drop {
398 t.Fatalf("packet after blackout dropped: %+v", event.Decision)
399 }
400 if got := string(awaitPacket(t, serverPackets).payload); got != "after" {
401 t.Fatalf("server received %q, want %q", got, "after")
402 }
403
404 stats := proxy.Stats()
405 if stats.ToServer.BlackoutDrops != 1 {
406 t.Fatalf("blackout drops = %d, want 1", stats.ToServer.BlackoutDrops)
407 }
408 if stats.ToServer.Received != 3 || stats.ToServer.Sent != 2 {
409 t.Fatalf("to-server stats = %s, want 3 received and 2 sent", stats.ToServer)
410 }
411}
412
413func TestProxyManualBlackoutToggles(t *testing.T) {
414 serverAddr, serverPackets := startSinkServer(t)
415 proxy, _, events := newTestProxy(t, Config{ServerAddr: serverAddr, Seed: 6})
416 client := newUDPClient(t)
417
418 proxy.SetBlackout(true)
419 send(t, client, proxy.Addr(), []byte("blacked"))
420 if event := awaitEvent(t, events); !event.Decision.Blackout {
421 t.Fatalf("decision = %+v, want a blackout drop", event.Decision)
422 }
423 expectNoDatagram(t, serverPackets)
424
425 proxy.SetBlackout(false)
426 send(t, client, proxy.Addr(), []byte("cleared"))
427 if event := awaitEvent(t, events); event.Decision.Drop {
428 t.Fatalf("decision = %+v, want forward", event.Decision)
429 }
430 if got := string(awaitPacket(t, serverPackets).payload); got != "cleared" {
431 t.Fatalf("server received %q, want %q", got, "cleared")
432 }
433}
434
435func TestProxyQueueCapacityBoundsInFlightPackets(t *testing.T) {
436 serverAddr, serverPackets := startSinkServer(t)
437 proxy, clock, events := newTestProxy(t, Config{
438 ServerAddr: serverAddr,
439 Seed: 11,
440 QueueCapacity: 2,
441 ToServer: LinkConfig{BaseLatency: time.Second},
442 })
443 client := newUDPClient(t)
444
445 observed := sendPackets(t, client, proxy.Addr(), events, 10, 32)
446 for _, event := range observed {
447 if event.Decision.Drop {
448 t.Fatalf("unexpected impairment drop: %+v", event.Decision)
449 }
450 }
451 stats := proxy.Stats()
452 if stats.ToServer.OverflowDrops != 8 {
453 t.Fatalf("overflow drops = %d, want 8", stats.ToServer.OverflowDrops)
454 }
455 if stats.ToServer.Sent != 0 {
456 t.Fatalf("sent = %d, want 0 before the latency elapses", stats.ToServer.Sent)
457 }
458 expectNoDatagram(t, serverPackets)
459
460 clock.Advance(time.Second)
461 for i := 0; i < 2; i++ {
462 awaitPacket(t, serverPackets)
463 }
464 if got := proxy.Stats().ToServer.Sent; got != 2 {
465 t.Fatalf("sent = %d, want 2", got)
466 }
467}
468
469func TestProxyDropsOversizeDatagrams(t *testing.T) {
470 serverAddr, serverPackets := startSinkServer(t)
471 proxy, _, events := newTestProxy(t, Config{
472 ServerAddr: serverAddr,
473 Seed: 8,
474 MaxPacketSize: 64,
475 })
476 client := newUDPClient(t)
477
478 send(t, client, proxy.Addr(), make([]byte, 65))
479 event := awaitEvent(t, events)
480 if !event.Decision.Drop || !event.Decision.Oversize {
481 t.Fatalf("decision = %+v, want an oversize drop", event.Decision)
482 }
483 expectNoDatagram(t, serverPackets)
484
485 stats := proxy.Stats()
486 if stats.ToServer.OversizeDrops != 1 || stats.ToServer.Received != 0 {
487 t.Fatalf("to-server stats = %s, want one oversize drop and no receipt", stats.ToServer)
488 }
489}
490
491func TestProxyRebindChangesUpstreamSourceAddress(t *testing.T) {
492 serverAddr, serverPackets := startSinkServer(t)
493 proxy, _, events := newTestProxy(t, Config{
494 ServerAddr: serverAddr,
495 Seed: 13,
496 NATRebindAfterPackets: 1,
497 })
498 client := newUDPClient(t)
499
500 send(t, client, proxy.Addr(), []byte("one"))
501 awaitEvent(t, events)
502 first := awaitPacket(t, serverPackets)
503
504 send(t, client, proxy.Addr(), []byte("two"))
505 awaitEvent(t, events)
506 second := awaitPacket(t, serverPackets)
507
508 if first.from.Port == second.from.Port {
509 t.Fatalf("upstream source port stayed %d across a NAT rebind", first.from.Port)
510 }
511 if got := proxy.Stats().Rebinds; got < 1 {
512 t.Fatalf("rebinds = %d, want at least 1", got)
513 }
514}
515
516func TestProxyExplicitRebindKeepsRoundTripWorking(t *testing.T) {
517 serverAddr, serverPackets := startEchoServer(t)
518 proxy, _, events := newTestProxy(t, Config{ServerAddr: serverAddr, Seed: 17})
519 client := newUDPClient(t)
520
521 send(t, client, proxy.Addr(), []byte("one"))
522 awaitEvent(t, events)
523 awaitEvent(t, events)
524 if got := string(readPacket(t, client)); got != "one" {
525 t.Fatalf("client received %q, want %q", got, "one")
526 }
527 before := awaitPacket(t, serverPackets)
528
529 if err := proxy.Rebind(); err != nil {
530 t.Fatalf("rebind: %v", err)
531 }
532
533 send(t, client, proxy.Addr(), []byte("two"))
534 awaitEvent(t, events)
535 awaitEvent(t, events)
536 if got := string(readPacket(t, client)); got != "two" {
537 t.Fatalf("client received %q, want %q", got, "two")
538 }
539 after := awaitPacket(t, serverPackets)
540
541 if before.from.Port == after.from.Port {
542 t.Fatalf("upstream source port stayed %d after Rebind", before.from.Port)
543 }
544 stats := proxy.Stats()
545 if stats.Rebinds != 1 || stats.StrayPackets != 0 {
546 t.Fatalf("unexpected stats after rebind: %s", stats)
547 }
548}
549
550func TestProxyCloseIsIdempotent(t *testing.T) {
551 serverAddr, _ := startSinkServer(t)
552 proxy, _, _ := newTestProxy(t, Config{ServerAddr: serverAddr})
553
554 if err := proxy.Close(); err != nil {
555 t.Fatalf("first Close: %v", err)
556 }
557 if err := proxy.Close(); err != nil {
558 t.Fatalf("second Close: %v", err)
559 }
560 if err := proxy.Rebind(); err != ErrClosed {
561 t.Fatalf("Rebind after Close = %v, want ErrClosed", err)
562 }
563}
564
565// TestProxyQUICLikeFlowUnderImpairment drives a QUIC-like packet flow (1.2 KiB
566// MTU-sized packets in bursts, with a blackout window in the middle) through a
567// fully impaired link and checks that every packet is accounted for exactly
568// once: received + duplicates == sent + drops.
569func TestProxyQUICLikeFlowUnderImpairment(t *testing.T) {
570 const (
571 phaseA = 120
572 phaseB = 30
573 phaseC = 150
574 packetSize = 1200
575 )
576 serverAddr, serverPackets := startSinkServer(t)
577 proxy, clock, events := newTestProxy(t, Config{
578 ServerAddr: serverAddr,
579 Seed: 7,
580 QueueCapacity: 1024,
581 ToServer: LinkConfig{
582 BaseLatency: 2 * time.Millisecond,
583 Jitter: time.Millisecond,
584 LossPercent: 10,
585 DuplicatePercent: 5,
586 DuplicateDelay: time.Millisecond,
587 ReorderPercent: 5,
588 ReorderDelay: 3 * time.Millisecond,
589 Blackouts: []Blackout{{Start: 10 * time.Millisecond, End: 20 * time.Millisecond}},
590 },
591 })
592 client := newUDPClient(t)
593
594 observed := sendPackets(t, client, proxy.Addr(), events, phaseA, packetSize)
595 clock.Advance(10 * time.Millisecond)
596
597 // Every packet inside the blackout window must be dropped.
598 blacked := sendPackets(t, client, proxy.Addr(), events, phaseB, packetSize)
599 for _, event := range blacked {
600 if !event.Decision.Blackout {
601 t.Fatalf("blackout phase decision = %+v, want a blackout drop", event.Decision)
602 }
603 }
604 clock.Advance(10 * time.Millisecond)
605
606 observed = append(observed, blacked...)
607 observed = append(observed, sendPackets(t, client, proxy.Addr(), events, phaseC, packetSize)...)
608 clock.Advance(20 * time.Millisecond)
609
610 var expected, dropped, duplicates, reordered uint64
611 for _, event := range observed {
612 if event.Decision.Drop {
613 dropped++
614 continue
615 }
616 expected += 1 + uint64(event.Decision.Duplicates)
617 duplicates += uint64(event.Decision.Duplicates)
618 if event.Decision.Reordered {
619 reordered++
620 }
621 }
622 for i := uint64(0); i < expected; i++ {
623 awaitPacket(t, serverPackets)
624 }
625
626 stats := proxy.Stats()
627 if stats.ToServer.Received != phaseA+phaseB+phaseC {
628 t.Fatalf("received = %d, want %d", stats.ToServer.Received, phaseA+phaseB+phaseC)
629 }
630 if stats.ToServer.BlackoutDrops != phaseB {
631 t.Fatalf("blackout drops = %d, want %d", stats.ToServer.BlackoutDrops, phaseB)
632 }
633 if stats.ToServer.OverflowDrops != 0 || stats.ToServer.OversizeDrops != 0 {
634 t.Fatalf("unexpected overflow/oversize: %s", stats.ToServer)
635 }
636 if stats.ToServer.Duplicates != duplicates {
637 t.Fatalf("duplicates = %d, want %d", stats.ToServer.Duplicates, duplicates)
638 }
639 if stats.ToServer.Reordered != reordered || reordered == 0 {
640 t.Fatalf("reordered = %d (want %d and non-zero)", stats.ToServer.Reordered, reordered)
641 }
642 if duplicates == 0 {
643 t.Fatal("flow produced no duplicates, so duplication was not exercised")
644 }
645 if stats.ToServer.LossDrops == 0 {
646 t.Fatal("flow produced no loss drops, so loss was not exercised")
647 }
648 if drops := stats.ToServer.LossDrops + stats.ToServer.BlackoutDrops + stats.ToServer.OversizeDrops + stats.ToServer.OverflowDrops; drops != dropped {
649 t.Fatalf("drops = %d, want %d", drops, dropped)
650 }
651 if stats.ToServer.Sent != expected {
652 t.Fatalf("sent = %d, want %d", stats.ToServer.Sent, expected)
653 }
654 // Oversize datagrams are rejected before Received is incremented and
655 // therefore don't participate in the accepted-packet identity.
656 total := stats.ToServer.LossDrops + stats.ToServer.BlackoutDrops + stats.ToServer.OverflowDrops
657 if got := stats.ToServer.Sent + total; got != stats.ToServer.Received+stats.ToServer.Duplicates {
658 t.Fatalf("accounting invariant broken: received(%d) + duplicates(%d) != sent(%d) + drops(%d)",
659 stats.ToServer.Received, stats.ToServer.Duplicates, stats.ToServer.Sent, total)
660 }
661 if got := stats.String(); got == "" {
662 t.Fatal("Stats.String returned empty")
663 }
664}