#!/usr/bin/env python3 """Self-test for analyse-pcap.py: synthesise a capture with known properties and check the tool reports them.""" import os, socket, struct, subprocess, sys, tempfile GC = socket.inet_aton("10.0.0.64") PC = socket.inet_aton("10.0.0.1") MAC_GC = bytes.fromhex("0009bf000001") MAC_PC = bytes.fromhex("002b67d4156c") def tcp_frame(src, dst, smac, dmac, seq, payload, win=4096): tcp = struct.pack("!HHIIBBHHH", 4000, 21, seq, 0, 5 << 4, 0x10, win, 0, 0) ip = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + len(tcp) + len(payload), 0, 0, 64, socket.IPPROTO_TCP, 0, src, dst) return dmac + smac + b"\x08\x00" + ip + tcp + payload def write_pcap(path, pkts): with open(path, "wb") as f: f.write(struct.pack(" reported {pct}% of the EXI ceiling") # Scenario B: small frames -- must trigger the per-frame-overhead warning. pkts, seq, t = [], 1, 2000.0 for i in range(1000): pkts.append((t, tcp_frame(GC, PC, MAC_GC, MAC_PC, seq, b"\0" * 200))) seq += 200 t += 0.002 pB = os.path.join(tmp, "b.pcap") write_pcap(pB, pkts) out = run(pB) if "PER-FRAME overhead" not in out: fails.append(f"B: small frames did not trigger the overhead warning\n{out}") else: print("B 200B payloads -> per-frame-overhead warning raised") # Scenario C: retransmissions (same seq+len twice) must be counted. pkts, t = [], 3000.0 for i in range(10): pkts.append((t, tcp_frame(GC, PC, MAC_GC, MAC_PC, 1, b"\0" * 1460))) t += 0.002 pC = os.path.join(tmp, "c.pcap") write_pcap(pC, pkts) out = run(pC) if "retransmissions 9" not in out: fails.append(f"C: expected 9 retransmissions\n{out}") else: print("C same seq x10 -> 9 retransmissions counted") # Scenario D: a long stall must be found and attributed. pkts, seq, t = [], 1, 4000.0 for i in range(100): pkts.append((t, tcp_frame(GC, PC, MAC_GC, MAC_PC, seq, b"\0" * 1460))) seq += 1460 t += 0.3 if i == 50 else 0.001 # one 300 ms hole pD = os.path.join(tmp, "d.pcap") write_pcap(pD, pkts) out = run(pD) if "300.00 ms" not in out: fails.append(f"D: the 300 ms gap was not reported\n{out}") elif "NOT sending" not in out: fails.append(f"D: dominant stall not attributed\n{out}") else: print("D one 300ms hole in 100 frames -> found and attributed") # Scenario E: zero-window from the GC means the GC cannot drain. pkts, t = [], 5000.0 pkts.append((t, tcp_frame(PC, GC, MAC_PC, MAC_GC, 1, b"\0" * 100, win=0))) pkts.append((t + 0.05, tcp_frame(PC, GC, MAC_PC, MAC_GC, 2, b"\0" * 100, win=4096))) pE = os.path.join(tmp, "e.pcap") write_pcap(pE, pkts) out = run(pE) if "zero-window" not in out or "CPU/stack bound" not in out: fails.append(f"E: zero-window not reported\n{out}") else: print("E zero-window span -> reported as CPU/stack bound") # Scenario F: pcapng must be refused with a useful message, not misparsed. pF = os.path.join(tmp, "f.pcapng") with open(pF, "wb") as f: f.write(struct.pack(">I", 0x0A0D0D0A) + b"\0" * 64) r = subprocess.run([sys.executable, "-I", "scripts/analyse-pcap.py", pF], capture_output=True, text=True) if "pcapng" not in (r.stdout + r.stderr): fails.append("F: pcapng not detected") else: print("F pcapng refused with a conversion hint") # Scenario G: window x RTT bound -- 2 undersized segments per ACK, which is # the real shape measured on hardware (1024 B segments, 2920 B window). # The tool must call it window-bound, and must warn that RX interrupt # COALESCING would make it worse rather than better. pkts, seq, t = [], 1, 6000.0 for i in range(200): pkts.append((t, tcp_frame(GC, PC, MAC_GC, MAC_PC, seq, b"\0" * 1024, win=2920))) seq += 1024 t += 0.0002 pkts.append((t, tcp_frame(GC, PC, MAC_GC, MAC_PC, seq, b"\0" * 1024, win=2920))) seq += 1024 t += 0.0002 pkts.append((t, tcp_frame(PC, GC, MAC_PC, MAC_GC, 1 + i, b"", win=65535))) t += 0.0024 # ~2.5 ms per round trip pG = os.path.join(tmp, "g.pcap") write_pcap(pG, pkts) out = run(pG, "--gc-ip", "10.0.0.64") if "2.0 data frames per ACK" not in out: fails.append(f"G: frames-per-ACK not derived\n{out}") elif "WINDOW x RTT BOUND" not in out: fails.append(f"G: window-bound flow not identified\n{out}") elif "COALESCING would make this WORSE" not in out: fails.append(f"G: missing the coalescing counter-warning\n{out}") elif "full-MSS segments" not in out: fails.append(f"G: did not suggest full-MSS segments\n{out}") else: print("G 1024B segments + 2920B window -> window x RTT bound,") print(" full-MSS suggested, coalescing warned against") def udp_frame(src, dst, smac, dmac, payload): udp = struct.pack("!HHHH", 7717, 7717, 8 + len(payload), 0) + payload ip = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + len(udp), 0, 0, 64, socket.IPPROTO_UDP, 0, src, dst) return dmac + smac + b"\x08\x00" + ip + udp # Scenario H: ROM streaming shape -- UDP/FSP, bulk flowing PC -> GC, with # small requests going the other way. Before UDP was counted this reported # zero throughput, because Swiss's in-game stack has no TCP at all. pkts, t = [], 7000.0 for i in range(300): pkts.append((t, udp_frame(GC, PC, MAC_GC, MAC_PC, b"\0" * 20))) # request t += 0.0001 for _ in range(2): # 2 queued pkts.append((t, udp_frame(PC, GC, MAC_PC, MAC_GC, b"\0" * 1446))) t += 0.0002 t += 0.0008 pH = os.path.join(tmp, "h.pcap") write_pcap(pH, pkts) out = run(pH, "--bulk", "to_gc") if "bulk direction: PC -> GC" not in out: fails.append(f"H: bulk direction not detected as PC->GC\n{out}") elif "UDP" not in out: fails.append(f"H: UDP frames not counted\n{out}") else: # 600 x 1446 B over ~0.39 s should be well above 1 MB/s kbs = float(out.split("throughput")[2].split("KB/s")[0].strip()) if kbs < 500: fails.append(f"H: UDP payload undercounted, {kbs} KB/s") else: print(f"H UDP/FSP streaming shape -> bulk PC->GC, {kbs:.0f} KB/s") print("\nFAILURES:\n " + "\n ".join(fails) if fails else "\nAll analyse-pcap self-tests passed.") sys.exit(1 if fails else 0)