TcpRun loses exactly one frame (loseFirst: the card's output buffer drops it; the sender's packet log still shows it, as with setDrop) in three new stories: lostSynAck, lostFin, lostLastFin. Measured: Java never repeats a SYN+ACK or a FIN. The server gives a handshake up after one timeout (3 RTT) and closes with a FIN from SYN_RCVD (its boundary check sometimes lets it wait twice; it means once). After its FIN a socket waits at most five times, each wait ended by a segment or the timeout; then it is gone. The core does the same (TCP_CLOSING_WAITS, with TIME_WAIT in the loop) and shows CLOSED where Java leaves the state standing (a named Java bug). One departure: a repeated SYN in SYN_RCVD gets the SYN+ACK once more and the wait starts anew; Java closes there, but its client never repeats the SYN. lostSynAck is compared up to the second SYN (JAVA_UNTIL); the rest is an engine test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.3 KiB
TypeScript
87 lines
3.3 KiB
TypeScript
// Test fixtures, ported from the private helpers of
|
|
// filius-ipad ios/FiliusPadTests/TopologyNetworkRuntimeEngineTests.swift.
|
|
|
|
import {
|
|
deviceConfiguration,
|
|
type EthernetFrame,
|
|
type TcpSegment,
|
|
NetworkRuntimeEngine,
|
|
portSnapshot,
|
|
topologySnapshot,
|
|
type DeviceConfiguration,
|
|
type Id,
|
|
type LinkSnapshot,
|
|
type NodeSnapshot,
|
|
type TopologySnapshotInput,
|
|
} from '../src/index.js';
|
|
|
|
let linkCounter = 0;
|
|
|
|
/** Swift uses a fresh `UUID()` per link; a counter keeps runs reproducible. */
|
|
export function link(sourceNodeId: Id, sourcePortId: Id, targetNodeId: Id, targetPortId: Id): LinkSnapshot {
|
|
linkCounter += 1;
|
|
return {
|
|
id: `ffffffff-0000-0000-0000-${linkCounter.toString(16).padStart(12, '0')}`,
|
|
sourceNodeId,
|
|
sourcePortId,
|
|
targetNodeId,
|
|
targetPortId,
|
|
};
|
|
}
|
|
|
|
export function pc(nodeId: Id, portId: Id): NodeSnapshot {
|
|
return { id: nodeId, kind: 'pc', ports: [portSnapshot(portId, 'eth0')] };
|
|
}
|
|
|
|
export function device(ipAddress: string, options: { mask?: string; gateway?: string } = {}): DeviceConfiguration {
|
|
return deviceConfiguration(ipAddress, options.mask ?? '255.255.255.0', options.gateway ?? '');
|
|
}
|
|
|
|
export function startedEngine(
|
|
input: TopologySnapshotInput,
|
|
seed: number = 14,
|
|
): NetworkRuntimeEngine {
|
|
const engine = new NetworkRuntimeEngine(seed);
|
|
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed, initialTimeMilliseconds: 0 });
|
|
return engine;
|
|
}
|
|
|
|
/** `00000000-0000-0000-0000-` followed by twelve hex digits. */
|
|
export function uuid(suffix: number): Id {
|
|
return `00000000-0000-0000-0000-${suffix.toString(16).toUpperCase().padStart(12, '0')}`;
|
|
}
|
|
|
|
const FRAME_EVENTS = new Set(['ethernetLinkDeparture', 'ethernetLinkDelivery', 'remoteLinkDelivery', 'arpAnswered']);
|
|
|
|
/**
|
|
* Advances until every frame in flight has arrived — what Swift's send did
|
|
* by itself; the engine's send returns at once, as Java's.
|
|
*/
|
|
export function letFramesArrive(engine: NetworkRuntimeEngine): void {
|
|
const inFlight = () =>
|
|
engine.state.pendingEvents.filter((event) => FRAME_EVENTS.has(event.kind.kind)).map((event) => event.deadlineMilliseconds);
|
|
for (let deadlines = inFlight(); deadlines.length > 0; deadlines = inFlight()) {
|
|
engine.handle({ type: 'advance', toMilliseconds: Math.min(...deadlines) });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The first frame the node sends whose TCP segment matches is lost, as with
|
|
* TcpRun's loseFirst: it leaves (the sender's card shows it) and never arrives.
|
|
*/
|
|
export function loseFirst(engine: NetworkRuntimeEngine, nodeId: Id, which: (segment: TcpSegment) => boolean): void {
|
|
type Depart = (fromNodeId: Id, portId: Id, endpoint: unknown, frame: EthernetFrame) => void;
|
|
const target = engine as unknown as { departEthernetFrame: Depart };
|
|
const depart = target.departEthernetFrame.bind(engine);
|
|
let lost = false;
|
|
target.departEthernetFrame = (fromNodeId, portId, endpoint, frame) => {
|
|
depart(fromNodeId, portId, endpoint, frame);
|
|
const payload = frame.payload.kind === 'ipv4' ? frame.payload.packet.payload : undefined;
|
|
if (lost || fromNodeId !== nodeId || payload?.kind !== 'tcp' || !which(payload.segment)) return;
|
|
lost = true;
|
|
engine.state.pendingEvents = engine.state.pendingEvents.filter(
|
|
(event) => !(event.kind.kind === 'ethernetLinkDelivery' && event.kind.frame.identity === frame.identity),
|
|
);
|
|
};
|
|
}
|