feat(core): TCP timing and closing after Java, measured with Java FILIUS

TCP follows Java's TCPSocket where it differs from Swift: each attempt and
TIME_WAIT wait three round trips that follow the speed (8250 ms at 90 %),
counted from when the segment is handed down, before ARP; the next segment
ends TIME_WAIT; the other end closes by itself on FIN; data no one
acknowledges closes with FIN; the accepted socket has the listener's
initial sequence number, and the listener takes the next.

Not taken from Java: three SYNs, not one (Java's retry loop ends after
the first timeout), named in tcpJava.test.ts as JAVA_BUGS.

tcpJava.test.ts compares TcpRun's three stories frame by frame and state
by state; the Swift comparison only checks that the TCP scenarios differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 11:17:44 +00:00
co-authored by Claude Opus 5.5
parent cd3c6a432c
commit cec0a92f95
8 changed files with 488 additions and 129 deletions
+4 -3
View File
@@ -13,7 +13,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) |
| `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft |
| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen |
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, Zeitgrenze 3 × 2,5 s mit höchstens drei Versuchen, Abbau über alle Java-Zustände samt `TIME_WAIT`), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, Remote Link, WLAN |
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, Remote Link, WLAN |
| `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
| `src/runtime/random.ts` | Engine, `TopologyJavaCompatibleRandom` | übertragen; BigInt für die 48-Bit-Arithmetik; dazu `nextDouble` (Javas `Math.random`, an OpenJDK 21 gemessen) |
@@ -27,8 +27,9 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `test/virtualFileSystem.test.ts`, `test/vfs.*.json` | `TopologyProjectPersistenceTests.swift`, `TopologyEditorReducerTests.swift` (drei Dateisystem-Tests, als Fälle) | übertragen und erweitert: 11 Fälle, Ergebnis aus dem Swift-Kern |
| `test/strings.test.ts`, `test/strings.*.json` | — | neu: Fälle und ganze Zeichenmengen, Ergebnis aus dem Swift-Kern |
| `test/engine.test.ts` | `TopologyNetworkRuntimeEngineTests.swift` (21 von 72 Tests) | übertragen; `nextInt`- und `nextDouble`-Referenzwerte an OpenJDK 21 gemessen; die DHCP-Client-Tests seit F2.3b nach Javas Verhalten (neun, davon zwei benannte Java-Fehler, die der Kern nicht übernimmt) |
| `test/oracle.test.ts`, `test/scenarios/` | — | neu: 14 Szenarien (davon 2 UDP, 3 TCP, 4 DHCP), Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`); Schritte für UDP, TCP und DHCP; die vier DHCP-Szenarien folgen seit F2.3b Java und müssen von Swift abweichen |
| `test/dhcpJava.test.ts`, `test/scenarios/*.java.json`, `test/scenario.ts` | — | neu: die DHCP-Szenarien gegen Java-Filius (`tools/java-oracle`, `DhcpRun`): je Client dieselben Frames in derselben Reihenfolge, Abstände auf 200 ms, dieselben Adressen am Ende |
| `test/oracle.test.ts`, `test/scenarios/` | — | neu: 14 Szenarien (davon 2 UDP, 3 TCP, 4 DHCP), Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`); Schritte für UDP, TCP und DHCP; die vier DHCP-Szenarien folgen seit F2.3b Java, die drei TCP-Szenarien seit F2.3c; sie müssen von Swift abweichen |
| `test/dhcpJava.test.ts`, `test/scenarios/*.java.json`, `test/scenario.ts`, `test/javaRun.ts` | — | neu: die DHCP-Szenarien gegen Java-Filius (`tools/java-oracle`, `DhcpRun`): je Client dieselben Frames in derselben Reihenfolge, Abstände auf 200 ms, dieselben Adressen am Ende |
| `test/tcpJava.test.ts`, `test/scenarios/tcp-java-*.java.json` | — | neu: drei TCP-Abläufe gegen Java-Filius (`TcpRun`): Frames mit Flags und Nummern, Socket-Zustände; ein benannter Java-Fehler (`JAVA_BUGS`: nur ein SYN) |
| `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument |
## packages/fls
+51 -12
View File
@@ -6,7 +6,10 @@
// without remote link, wireless and gateway NAT; with the firewall)
// "UDP sockets and Router RIP" (lines ~4472–5187, the
// UDP sockets; RIP not yet)
// "Java-compatible TCP sockets" (lines ~5187–5913)
// "Java-compatible TCP sockets" (lines ~5187–5913; the
// timing and closing after Java's TCPSocket, measured with Java
// FILIUS: 3 round trips per attempt and for TIME_WAIT, the other end
// closes by itself on FIN, failed data closes with FIN)
// "DHCP server and automatic client" (lines ~5913–6694; the
// client rebuilt after Java's DHCPClient, measured with Java FILIUS:
// one message per attempt, an ARP check that waits a round trip,
@@ -366,8 +369,6 @@ function sameIgnoringCase(lhs: string | undefined, rhs: string | undefined): boo
export const TCP_MAXIMUM_SEGMENT_SIZE = 1_460;
export const TCP_MAXIMUM_SEND_ATTEMPTS = 3;
export const TCP_ROUND_TRIP_TIME_MILLISECONDS = 2_500;
export const TCP_DEFAULT_TIMEOUT_MILLISECONDS = TCP_MAXIMUM_SEND_ATTEMPTS * TCP_ROUND_TRIP_TIME_MILLISECONDS;
export const TCP_EPHEMERAL_PORT_LOWER_BOUND = 49_152;
export const TCP_EPHEMERAL_PORT_UPPER_BOUND_EXCLUSIVE = 65_535;
@@ -1330,7 +1331,8 @@ export class NetworkRuntimeEngine {
tcpState: 'SYN_RCVD',
parentListenerSocketId: listener.id,
});
const session = this.makeTcpSession(acceptedId, 'SYN_RCVD');
// Java accepts on the socket that listened: it has the listener's initial sequence number.
const session = this.makeTcpSession(acceptedId, 'SYN_RCVD', state.tcpSessionsById.get(listener.id)?.nextSendSequenceNumber);
session.remoteSequenceNumber = nextTcpSequenceNumber(segment);
state.tcpSessionsById.set(acceptedId, session);
getOrCreate(state.tcpAcceptedSocketIdsByListenerId, listener.id, () => []).push(acceptedId);
@@ -1370,14 +1372,28 @@ export class NetworkRuntimeEngine {
this.sendTcpAcknowledgement(socketId, segment);
return;
}
case 'SYN_RCVD':
case 'SYN_RCVD': {
if (!has(TCP_FLAG.acknowledgement) || !this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber)) return;
this.updateTcpState(socketId, 'ESTABLISHED');
// Java: the socket that listened is the connection now; the server
// listens on with a new socket, and a new initial sequence number.
const listenerId = socket.parentListenerSocketId;
if (listenerId !== undefined && this.stateValue.tcpSessionsById.has(listenerId)) {
this.stateValue.tcpSessionsById.set(listenerId, this.makeTcpSession(listenerId, 'LISTEN'));
}
return;
}
case 'TIME_WAIT':
this.clearTcpPendingTransmission(socketId);
this.updateTcpState(socketId, 'CLOSED');
return;
case 'ESTABLISHED':
if (has(TCP_FLAG.finish)) {
this.sendTcpAcknowledgement(socketId, segment);
this.updateTcpState(socketId, 'CLOSE_WAIT');
// Java's socket thread leaves listen once the state is no longer
// ESTABLISHED and closes by itself, without waiting for the application.
this.closeTcpSocket(socketId);
} else if (segment.payload.length > 0 || has(TCP_FLAG.push)) {
this.receiveTcpPayload(socketId, segment);
} else if (has(TCP_FLAG.acknowledgement)) {
@@ -1453,6 +1469,9 @@ export class NetworkRuntimeEngine {
if (socket === undefined || socket.remoteIPAddress === undefined || socket.remotePort === undefined || session === undefined) {
return undefined;
}
// Java's socket starts waiting when it hands the segment down, before
// ARP has found the next hop; the timeout counts from here.
const handedDownAt = state.currentTimeMilliseconds;
let segment: TcpSegment;
if (retransmission && session.lastSentSegment !== undefined) {
segment = session.lastSentSegment;
@@ -1500,14 +1519,19 @@ export class NetworkRuntimeEngine {
detail: retransmission ? `${detail} retransmission attempt ${session.sendAttempts}` : detail,
});
const delivery = this.sendIPv4Packet(socket.nodeId, packet, networkInterface.portId);
if (state.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined) this.scheduleTcpTimeoutIfNeeded(socketId);
if (state.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined) this.scheduleTcpTimeoutIfNeeded(socketId, handedDownAt);
return delivery;
}
private scheduleTcpTimeoutIfNeeded(socketId: Id): void {
private scheduleTcpTimeoutIfNeeded(socketId: Id, fromMilliseconds: number): void {
const pending = this.stateValue.pendingEvents.some((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId);
if (pending) return;
this.schedule(this.stateValue.currentTimeMilliseconds + TCP_DEFAULT_TIMEOUT_MILLISECONDS, { kind: 'tcpTimeout', sessionId: socketId });
this.schedule(fromMilliseconds + this.tcpTimeoutMilliseconds(), { kind: 'tcpTimeout', sessionId: socketId });
}
/** Java's defaultTimeout: MAX_SENDEVERSUCHE round trips, for each attempt and for TIME_WAIT. */
private tcpTimeoutMilliseconds(): number {
return TCP_MAXIMUM_SEND_ATTEMPTS * this.runtimeSpeed.roundTripTimeMilliseconds;
}
private handleTcpTimeout(socketId: Id): void {
@@ -1523,7 +1547,14 @@ export class NetworkRuntimeEngine {
if (session === undefined || last === undefined) return;
if (session.sendAttempts >= TCP_MAXIMUM_SEND_ATTEMPTS) {
this.clearTcpPendingTransmission(socketId);
// Java: data that no one acknowledges (sent only while ESTABLISHED)
// ends senden with schliessen, an orderly close with FIN. A handshake
// or a FIN that fails closes at once.
if (socket.tcpState === 'ESTABLISHED') {
this.closeTcpSocket(socketId);
} else {
this.updateTcpState(socketId, 'CLOSED');
}
return;
}
const synchronize = (last.flags & TCP_FLAG.synchronize) !== 0;
@@ -1560,10 +1591,11 @@ export class NetworkRuntimeEngine {
);
}
/** Java's close loop: TIME_WAIT ends with the next segment, else after defaultTimeout. */
private enterTcpTimeWait(socketId: Id): void {
this.clearTcpPendingTransmission(socketId);
this.updateTcpState(socketId, 'TIME_WAIT');
this.schedule(this.stateValue.currentTimeMilliseconds + TCP_ROUND_TRIP_TIME_MILLISECONDS, { kind: 'tcpTimeout', sessionId: socketId });
this.schedule(this.stateValue.currentTimeMilliseconds + this.tcpTimeoutMilliseconds(), { kind: 'tcpTimeout', sessionId: socketId });
}
/**
@@ -1579,8 +1611,12 @@ export class NetworkRuntimeEngine {
const socket = this.stateValue.socketsById.get(socketId);
if (socket === undefined || socket.tcpState === undefined) return advancedToTimeout;
const waitingOnState = options.waitingStates?.includes(socket.tcpState) === true;
// Data waits for its acknowledgement only while connected: once the
// socket closes (Java: senden gives up and calls schliessen), it returns.
const waitingOnAck =
options.waitingForAcknowledgement === true && this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined;
options.waitingForAcknowledgement === true &&
socket.tcpState === 'ESTABLISHED' &&
this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined;
if (!waitingOnState && !waitingOnAck) return advancedToTimeout;
const deadlines = this.stateValue.pendingEvents
.filter((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId)
@@ -1637,8 +1673,11 @@ export class NetworkRuntimeEngine {
return Number(BigInt.asUintN(32, (value % 4_294_967_296n) * 1_000_000n));
}
private makeTcpSession(socketId: Id, socketState: TcpSocketState): TcpSession {
const initialSequence = this.allocateTcpInitialSequenceNumber();
private makeTcpSession(
socketId: Id,
socketState: TcpSocketState,
initialSequence: number = this.allocateTcpInitialSequenceNumber(),
): TcpSession {
return {
id: socketId,
socketId,
+16 -105
View File
@@ -1,133 +1,44 @@
// DHCP as Java FILIUS runs it. tools/java-oracle's DhcpRun built each
// scenario's network in Java FILIUS, ran it and wrote what every network card
// saw (<name>.java.json, FILIUS' own packet log). The engine runs the same
// network. For each DHCP client, the frames that concern it must come in the
// same order, with the same content, at about the same distance from each
// other, and every host must end with the same addresses.
// saw (<name>.java.json). The engine runs the same network. For each DHCP
// client, the frames that concern it must come in the same order, with the
// same content, at about the same distance from each other (javaRun.ts), and
// every host must end with the same addresses.
//
// A frame counts when it first shows on any card (sent, or received), in
// Java and in the engine alike. Not only on the client's own card: there,
// FILIUS' log misses the DISCOVER of the client that sends second when two
// start together (measured in four of four runs of dhcp-lan; the server
// receives it) — a fault of the log, not of the network. Java logs no frame
// identity; the same text within DUPLICATE_WINDOW_MILLISECONDS is one frame
// (measured: a frame reaches the last card within some 130 ms; the closest
// two distinct frames of the same text are the DISCOVERs around a DECLINE,
// some 520 ms apart).
//
// Java runs on threads in real time, at the default speed of 90 % (55 ms a
// cable, a round trip time of 2750 ms). Its gaps match the engine's within a
// few milliseconds, except where a card sends two frames at once: Java's
// cable carries one frame at a time (SimplexVerbindung sleeps the delay with
// the port's buffer locked, and the sender waits), the engine's cable all of
// them together — up to three cables' delay, measured. A gap may differ by
// GAP_TOLERANCE_MILLISECONDS; a wrong round trip time (250 ms) may not. Java draws the link-local
// address with Math.random; it is compared as 169.254.1.x. Where several
// clients start together, which one is first varies in Java (the first
// thread pays for loading the classes); a client is therefore compared
// without the other clients' frames and the answers to them.
// Java draws the link-local address with Math.random; it is compared as
// 169.254.1.x. Where several clients start together, which one is first
// varies in Java (the first thread pays for loading the classes); a client is
// therefore compared without the other clients' frames and the answers to
// them.
import { readdirSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { NetworkRuntimeEngine, stableMacAddress, type EthernetFrame } from '../src/index.js';
import { snapshotOf, type Scenario } from './scenario.js';
import { stableMacAddress } from '../src/index.js';
import { GAP_TOLERANCE_MILLISECONDS, javaFrames, watchedEngine, type JavaFrame, type SeenFrame } from './javaRun.js';
import { type Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const GAP_TOLERANCE_MILLISECONDS = 200;
const DUPLICATE_WINDOW_MILLISECONDS = 300;
interface JavaFrame {
time: number;
source: string;
destination: string;
arp?: number;
senderIp?: string;
targetIp?: string;
sourceIp?: string;
destinationIp?: string;
protocol?: number;
sourcePort?: number;
destinationPort?: number;
payload?: string;
icmp?: number;
}
interface JavaRun {
frames: Record<string, JavaFrame[]>;
devices: Record<string, [string, string, string, string]>;
}
interface SeenFrame {
readonly time: number;
readonly text: string;
}
const LINK_LOCAL = /169\.254\.1\.\d+/g;
function javaText(frame: JavaFrame): string {
const macs = `${frame.source} > ${frame.destination}`.toUpperCase();
if (frame.arp !== undefined) return `${macs} ARP ${frame.arp === 1 ? 'request' : 'reply'} ${frame.senderIp} > ${frame.targetIp}`;
if (frame.icmp !== undefined) return `${macs} ICMP ${frame.icmp}`;
return `${macs} UDP ${frame.sourceIp}:${frame.sourcePort} > ${frame.destinationIp}:${frame.destinationPort} ${frame.payload}`;
}
function engineText(frame: EthernetFrame): string {
const macs = `${frame.sourceMacAddress} > ${frame.destinationMacAddress}`.toUpperCase();
if (frame.payload.kind === 'arp') {
const arp = frame.payload.packet;
return `${macs} ARP ${arp.operation} ${arp.senderIPAddress} > ${arp.targetIPAddress}`;
}
const packet = frame.payload.packet;
if (packet.payload.kind !== 'udp') return `${macs} ${packet.payload.kind.toUpperCase()}`;
const datagram = packet.payload.datagram;
return `${macs} UDP ${packet.senderIPAddress}:${datagram.sourcePort} > ${packet.receiverIPAddress}:${datagram.destinationPort} ${new TextDecoder().decode(datagram.payload)}`;
}
/** The engine's run of the scenario's network, without its steps, for `milliseconds`. */
function runEngine(scenario: Scenario, milliseconds: number) {
const engine = new NetworkRuntimeEngine(scenario.seed);
const framesByIdentity = new Map<number, EthernetFrame>();
const send = engine.sendEthernetFrame.bind(engine);
engine.sendEthernetFrame = (fromNodeId, outgoingPortId, frame) => {
framesByIdentity.set(frame.identity, frame);
send(fromNodeId, outgoingPortId, frame);
};
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
const { engine, frames } = watchedEngine(scenario);
engine.handle({ type: 'advance', toMilliseconds: milliseconds });
// Every frame when it first shows on a host's card (as FILIUS logs it: sent, or received).
const hosts = new Set(scenario.nodes.filter((node) => node.kind !== 'networkSwitch').map((node) => node.id));
const firstSeen = new Map<number, SeenFrame>();
for (const event of engine.state.packetTraces) {
if (event.layer !== 'physical' || !hosts.has(event.nodeId) || event.frameIdentity === undefined) continue;
if (!((event.direction === 'outbound' && event.operation === 'sent') || (event.direction === 'inbound' && event.operation === 'received'))) continue;
if (firstSeen.has(event.frameIdentity)) continue;
firstSeen.set(event.frameIdentity, { time: event.timeMilliseconds, text: engineText(framesByIdentity.get(event.frameIdentity)!) });
}
const devices = Object.fromEntries(
[...engine.state.topologySnapshot.deviceConfigurations].map(([node, device]) => [
node,
[device.ipAddress, device.subnetMask, device.defaultGateway, device.dnsServer],
]),
);
return { frames: [...firstSeen.values()], devices };
}
/** Java's frames of all cards, each once: the same text within the window is the same frame. */
function javaFrames(java: JavaRun): SeenFrame[] {
const all = Object.values(java.frames)
.flat()
.map((frame) => ({ time: frame.time, text: javaText(frame) }))
.sort((lhs, rhs) => lhs.time - rhs.time);
const frames: SeenFrame[] = [];
for (const frame of all) {
if (frames.some((each) => each.text === frame.text && frame.time - each.time <= DUPLICATE_WINDOW_MILLISECONDS)) continue;
frames.push(frame);
}
return frames;
return { frames: frames(), devices };
}
/** Consecutive frames without the other clients' own: the text, and the time since the one before. */
@@ -147,7 +58,7 @@ function clientMac(scenario: Scenario, nodeId: string): string {
}
const runs = readdirSync(scenarioDirectory)
.filter((name) => name.endsWith('.java.json'))
.filter((name) => name.startsWith('dhcp-') && name.endsWith('.java.json'))
.map((name) => name.replace('.java.json', ''))
.sort();
@@ -166,7 +77,7 @@ describe('DHCP as Java FILIUS runs it', () => {
const clients = scenario.dhcpClients ?? [];
for (const client of clients) {
const others = clients.filter((each) => each !== client).map((each) => clientMac(scenario, each));
const expected = steps(javaFrames(java), others);
const expected = steps(javaFrames(java.frames), others);
const actual = steps(engine.frames, others);
for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) {
expect(actual[index]?.text, `${client} frame #${index + 1}`).toBe(expected[index]?.text);
+71 -6
View File
@@ -26,7 +26,6 @@ import {
stableMacAddress,
switchConfiguration,
TCP_FLAG,
TCP_ROUND_TRIP_TIME_MILLISECONDS,
tcpFlagDescription,
topologySnapshot,
traceRetentionPolicy,
@@ -899,7 +898,7 @@ describe('TCP sockets', () => {
expect(tcpFlagDescription(0)).toBe('');
});
it("closes with Java's FIN states, and TIME_WAIT releases both ends", () => {
it("closes with Java's FIN states: the other end closes by itself, TIME_WAIT lasts three round trips", () => {
const { engine, clientId, serverId } = pair(0x1761, 0x1762, '10.20.0');
const listenerId = engine.openTcpServerSocket(serverId, 9090)!;
const clientSocketId = engine.openTcpClientSocket(clientId, '10.20.0.2', 9090)!;
@@ -907,16 +906,82 @@ describe('TCP sockets', () => {
const serverSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
engine.closeTcpSocket(clientSocketId);
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('FIN_WAIT_2');
expect(engine.state.socketsById.get(serverSocketId)?.tcpState).toBe('CLOSE_WAIT');
engine.closeTcpSocket(serverSocketId);
// Java's socket thread leaves listen on the FIN and closes without the application.
const serverStates = engine.state.packetTraces
.filter((trace) => trace.nodeId === serverId && trace.detail?.startsWith('TCP ') === true && trace.detail.includes(' -> '))
.map((trace) => trace.detail);
expect(serverStates.slice(-3)).toEqual(['TCP ESTABLISHED -> CLOSE_WAIT', 'TCP CLOSE_WAIT -> LAST_ACK', 'TCP LAST_ACK -> CLOSED']);
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
engine.closeTcpSocket(serverSocketId);
expect(engine.state.socketsById.get(serverSocketId)?.tcpState).toBe('CLOSED');
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + TCP_ROUND_TRIP_TIME_MILLISECONDS });
// Java's defaultTimeout: three round trips of 2750 ms at 90 %.
const timeWait = engine.state.packetTraces.find((trace) => trace.nodeId === clientId && trace.detail?.endsWith('-> TIME_WAIT') === true)!
.timeMilliseconds;
engine.handle({ type: 'advance', toMilliseconds: timeWait + 3 * 2_750 - 1 });
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
engine.handle({ type: 'advance', toMilliseconds: timeWait + 3 * 2_750 });
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('CLOSED');
expect(engine.state.socketsById.get(listenerId)?.tcpState).toBe('LISTEN');
});
it('ends TIME_WAIT with the next segment that comes', () => {
const { engine, clientId, serverId } = pair(0x1771, 0x1772, '10.21.0');
engine.openTcpServerSocket(serverId, 9090);
const clientSocketId = engine.openTcpClientSocket(clientId, '10.21.0.2', 9090)!;
expect(engine.connectTcp(clientSocketId)).toBe(true);
engine.closeTcpSocket(clientSocketId);
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
const localPort = engine.state.socketsById.get(clientSocketId)!.localPort;
engine.sendIPv4Packet(serverId, {
identity: 77,
senderIPAddress: '10.21.0.2',
receiverIPAddress: '10.21.0.1',
timeToLive: 64,
protocol: 'tcp',
payload: {
kind: 'tcp',
segment: { sourcePort: 9090, destinationPort: localPort, sequenceNumber: 1, acknowledgementNumber: 0, flags: TCP_FLAG.acknowledgement, payload: new Uint8Array() },
},
});
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 100 });
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('CLOSED');
});
it('accepts on the socket that listened, and listens on with the next sequence number', () => {
const [firstId, firstPortId, secondId, secondPortId, serverId, serverPortId, switchId] = [
0x1781, 0x1782, 0x1783, 0x1784, 0x1785, 0x1786, 0x1787,
].map(uuid) as [string, string, string, string, string, string, string];
const switchPorts = [0x1791, 0x1792, 0x1793].map(uuid) as [string, string, string];
const engine = startedEngine({
nodes: [
pc(firstId, firstPortId),
pc(secondId, secondPortId),
pc(serverId, serverPortId),
{ id: switchId, kind: 'networkSwitch', ports: switchPorts.map((id, index) => portSnapshot(id, `sw${index + 1}`)) },
],
links: [
link(firstId, firstPortId, switchId, switchPorts[0]),
link(secondId, secondPortId, switchId, switchPorts[1]),
link(serverId, serverPortId, switchId, switchPorts[2]),
],
deviceConfigurations: new Map([
[firstId, device('10.22.0.1')],
[secondId, device('10.22.0.2')],
[serverId, device('10.22.0.3')],
]),
});
// Java counts initial sequence numbers over all TCP sockets: the listener 1, the first client 2,
// the listener's successor 3 (made when the first is accepted), the second client 4.
const listenerId = engine.openTcpServerSocket(serverId, 80)!;
expect(engine.connectTcp(engine.openTcpClientSocket(firstId, '10.22.0.3', 80)!)).toBe(true);
expect(engine.connectTcp(engine.openTcpClientSocket(secondId, '10.22.0.3', 80)!)).toBe(true);
const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)!;
expect(accepted.map((id) => engine.state.tcpSessionsById.get(id)?.nextSendSequenceNumber)).toEqual([1_000_001, 3_000_001]);
});
});
describe('DHCP', () => {
+134
View File
@@ -0,0 +1,134 @@
// What tools/java-oracle's DhcpRun and TcpRun recorded in Java FILIUS
// (<name>.java.json: every frame each card saw, FILIUS' own packet log), and
// the same view of an engine run: each frame when it first shows on any
// host's card, as a line of text. Shared by dhcpJava.test.ts and
// tcpJava.test.ts.
//
// Not only the frames of one card: FILIUS' log misses the DISCOVER of the
// DHCP client that sends second when two start together (measured in four of
// four runs of dhcp-lan; the server receives it) — a fault of the log, not of
// the network. Java logs no frame identity; the same text within
// DUPLICATE_WINDOW_MILLISECONDS is one frame (measured: a frame reaches the
// last card within some 130 ms; the closest two distinct frames of the same
// text are the DISCOVERs around a DECLINE, some 520 ms apart).
//
// Java runs on threads in real time, at the default speed of 90 % (55 ms a
// cable, a round trip time of 2750 ms). Its gaps match the engine's within a
// few milliseconds, except where a card sends two frames at once: Java's
// cable carries one frame at a time (SimplexVerbindung sleeps the delay with
// the port's buffer locked, and the sender waits), the engine's cable all of
// them together — up to three cables' delay, measured. A gap may differ by
// GAP_TOLERANCE_MILLISECONDS; a wrong round trip time (250 ms) may not.
import { NetworkRuntimeEngine, TCP_FLAG, type EthernetFrame } from '../src/index.js';
import { snapshotOf, type Scenario } from './scenario.js';
export const GAP_TOLERANCE_MILLISECONDS = 200;
const DUPLICATE_WINDOW_MILLISECONDS = 300;
export interface JavaFrame {
time: number;
source: string;
destination: string;
arp?: number;
senderIp?: string;
targetIp?: string;
sourceIp?: string;
destinationIp?: string;
protocol?: number;
sourcePort?: number;
destinationPort?: number;
payload?: string | null;
icmp?: number;
tcpFlags?: string;
sequence?: number;
acknowledgement?: number;
}
export interface SeenFrame {
readonly time: number;
readonly text: string;
}
function javaText(frame: JavaFrame): string {
const macs = `${frame.source} > ${frame.destination}`.toUpperCase();
if (frame.arp !== undefined) return `${macs} ARP ${frame.arp === 1 ? 'request' : 'reply'} ${frame.senderIp} > ${frame.targetIp}`;
if (frame.icmp !== undefined) return `${macs} ICMP ${frame.icmp}`;
const ends = `${frame.sourceIp}:${frame.sourcePort} > ${frame.destinationIp}:${frame.destinationPort}`;
if (frame.tcpFlags !== undefined) {
return `${macs} TCP ${ends} ${frame.tcpFlags || '-'} seq=${frame.sequence} ack=${frame.acknowledgement} length=${(frame.payload ?? '').length}`;
}
return `${macs} UDP ${ends} ${frame.payload}`;
}
const FLAG_LETTERS: [number, string][] = [
[TCP_FLAG.synchronize, 'S'],
[TCP_FLAG.acknowledgement, 'A'],
[TCP_FLAG.finish, 'F'],
[TCP_FLAG.push, 'P'],
[TCP_FLAG.reset, 'R'],
];
function engineText(frame: EthernetFrame): string {
const macs = `${frame.sourceMacAddress} > ${frame.destinationMacAddress}`.toUpperCase();
if (frame.payload.kind === 'arp') {
const arp = frame.payload.packet;
return `${macs} ARP ${arp.operation} ${arp.senderIPAddress} > ${arp.targetIPAddress}`;
}
const packet = frame.payload.packet;
if (packet.payload.kind === 'icmp') return `${macs} ICMP`;
if (packet.payload.kind === 'tcp') {
const segment = packet.payload.segment;
const flags = FLAG_LETTERS.filter(([bit]) => (segment.flags & bit) !== 0).map(([, letter]) => letter).join('');
return `${macs} TCP ${packet.senderIPAddress}:${segment.sourcePort} > ${packet.receiverIPAddress}:${segment.destinationPort} ${flags || '-'} seq=${segment.sequenceNumber} ack=${segment.acknowledgementNumber} length=${segment.payload.length}`;
}
const datagram = packet.payload.datagram;
return `${macs} UDP ${packet.senderIPAddress}:${datagram.sourcePort} > ${packet.receiverIPAddress}:${datagram.destinationPort} ${new TextDecoder().decode(datagram.payload)}`;
}
/** A started engine on the scenario's network whose frames can be read afterwards as FILIUS logs them. */
export function watchedEngine(scenario: Scenario) {
const engine = new NetworkRuntimeEngine(scenario.seed);
const framesByIdentity = new Map<number, EthernetFrame>();
const send = engine.sendEthernetFrame.bind(engine);
engine.sendEthernetFrame = (fromNodeId, outgoingPortId, frame) => {
framesByIdentity.set(frame.identity, frame);
send(fromNodeId, outgoingPortId, frame);
};
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
/** Every frame when it first shows on a host's card (sent, or received). */
const frames = (): SeenFrame[] => {
const hosts = new Set(scenario.nodes.filter((node) => node.kind !== 'networkSwitch').map((node) => node.id));
const firstSeen = new Map<number, SeenFrame>();
for (const event of engine.state.packetTraces) {
if (event.layer !== 'physical' || !hosts.has(event.nodeId) || event.frameIdentity === undefined) continue;
if (!((event.direction === 'outbound' && event.operation === 'sent') || (event.direction === 'inbound' && event.operation === 'received'))) {
continue;
}
if (firstSeen.has(event.frameIdentity)) continue;
firstSeen.set(event.frameIdentity, { time: event.timeMilliseconds, text: engineText(framesByIdentity.get(event.frameIdentity)!) });
}
return [...firstSeen.values()];
};
return { engine, frames };
}
/** Java's frames of all cards, each once: the same text within the window is the same frame. */
export function javaFrames(framesByNode: Record<string, JavaFrame[]>): SeenFrame[] {
const all = Object.values(framesByNode)
.flat()
.map((frame) => ({ time: frame.time, text: javaText(frame) }))
.sort((lhs, rhs) => lhs.time - rhs.time);
const frames: SeenFrame[] = [];
for (const frame of all) {
if (frames.some((each) => each.text === frame.text && frame.time - each.time <= DUPLICATE_WINDOW_MILLISECONDS)) continue;
frames.push(frame);
}
return frames;
}
/** Consecutive frames: the text, and the time since the one before. */
export function gaps(frames: readonly SeenFrame[]) {
return frames.map((frame, index) => ({ text: frame.text, gap: index === 0 ? 0 : frame.time - frames[index - 1]!.time }));
}
+5 -1
View File
@@ -285,7 +285,8 @@ const SWIFT_ERRORS: Record<string, (expected: Record<string, unknown>) => Record
/**
* Scenarios where the engine follows Java instead of Swift; the Java
* comparison checks them (dhcpJava.test.ts, measured with Java FILIUS). Here
* comparisons check that behaviour (dhcpJava.test.ts, tcpJava.test.ts,
* measured with Java FILIUS). Here
* they must differ from Swift, or the departure is not real.
*/
const JAVA_INSTEAD_OF_SWIFT: Record<string, string> = {
@@ -293,6 +294,9 @@ const JAVA_INSTEAD_OF_SWIFT: Record<string, string> = {
'dhcp-two-servers': 'DHCP client: the second OFFER is read and dropped, no REQUEST twice (Swift: answered with NAK)',
'dhcp-conflict': 'DHCP client: the ARP check finds the address taken and declines (Swift: it does not wait, two cards share the address)',
'dhcp-no-server': 'DHCP client: a link-local address after five timeouts (Swift: the old address after ten)',
'tcp-lan': 'TCP: the other end closes by itself on FIN, TIME_WAIT lasts 3 round trips, the accepted socket has the listener\'s sequence number',
'tcp-routed-loss': 'TCP: 3 round trips per attempt follow the speed (8250 ms at 90 %), failed data closes with FIN',
'tcp-closing': 'TCP: TIME_WAIT lasts 3 round trips and ends with the next segment; the other end closes by itself',
};
describe('same result as the Swift engine', () => {
+204
View File
@@ -0,0 +1,204 @@
// TCP as Java FILIUS runs it. tools/java-oracle's TcpRun played stories of
// socket calls on tcp-lan's network in Java FILIUS and wrote every frame each
// card saw, and every state each socket went through (tcp-java-<story>.java.json).
// The engine plays the same stories. The frames must come in the same order,
// with the same content, at about the same distance from each other
// (javaRun.ts); each state Java saw must come at about the same time.
//
// Java polled the states every 2 ms from another thread; a state the engine
// enters and leaves within STATE_UNSEEN_MILLISECONDS (CLOSE_WAIT, when the
// passive side closes by itself) Java may not have seen. The free port a
// client draws differs (Java's random, the engine's seed); it is compared as
// "client". Times count from the client's SYN.
//
// The engine's calls block on its virtual clock one after the other, where
// Java's server answers in a thread of its own: the server's answer starts
// when the client's send returns, one cable's delay later than in Java.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import type { Id, NetworkRuntimeEngine } from '../src/index.js';
import { GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame, type SeenFrame } from './javaRun.js';
import type { Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const STATE_UNSEEN_MILLISECONDS = 20;
const SERVER = '00000000-0000-0000-0000-000000000101';
const CLIENT = '00000000-0000-0000-0000-000000000102';
const OTHER_CLIENT = '00000000-0000-0000-0000-000000000103';
interface JavaRun {
frames: Record<string, JavaFrame[]>;
events: [number, string][];
states: Record<string, [number, string][]>;
}
const text = (value: string) => new TextEncoder().encode(value);
/** The story as the engine plays it; the nodes whose socket stands for Java's "client" and "server". */
interface Story {
readonly client: Id;
readonly play: (engine: NetworkRuntimeEngine, advanceTo: (milliseconds: number) => void) => void;
}
const STORIES: Record<string, Story> = {
exchange: {
client: CLIENT,
play: (engine, advanceTo) => {
const listener = engine.openTcpServerSocket(SERVER, 80)!;
advanceTo(500);
const client = engine.openTcpClientSocket(CLIENT, '192.168.0.10', 80)!;
engine.connectTcpWithResult(client);
const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listener)![0]!;
engine.sendTcp(client, text('GET /'));
engine.receiveTcp(accepted);
engine.sendTcp(accepted, text('x'.repeat(3000)));
engine.receiveTcp(client);
engine.closeTcpSocket(client);
// The server closes a second after it saw CLOSE_WAIT.
while (engine.state.socketsById.get(accepted)?.tcpState === 'ESTABLISHED') {
advanceTo(engine.state.currentTimeMilliseconds + 1);
}
advanceTo(engine.state.currentTimeMilliseconds + 1_000);
engine.closeTcpSocket(accepted);
},
},
closedPort: {
client: OTHER_CLIENT,
play: (engine, advanceTo) => {
advanceTo(500);
const client = engine.openTcpClientSocket(OTHER_CLIENT, '192.168.0.10', 81)!;
engine.connectTcpWithResult(client);
},
},
lostData: {
client: CLIENT,
play: (engine, advanceTo) => {
engine.openTcpServerSocket(SERVER, 80);
advanceTo(500);
const client = engine.openTcpClientSocket(CLIENT, '192.168.0.10', 80)!;
engine.connectTcpWithResult(client);
advanceTo(engine.state.currentTimeMilliseconds + 500);
engine.setGlobalPacketLossEnabled(true);
engine.sendTcp(client, text('verloren'));
engine.setGlobalPacketLossEnabled(false);
},
},
};
/** The states of the node's connection socket, from the engine's traces: when each was entered. */
function engineStates(engine: NetworkRuntimeEngine, nodeId: Id, fromEstablished: boolean): [number, string][] {
const transitions = engine.state.packetTraces
.filter((trace) => trace.nodeId === nodeId && trace.detail?.startsWith('TCP ') === true && trace.detail.includes(' -> '))
.map((trace) => [trace.timeMilliseconds, trace.detail!.slice(4).split(' -> ')] as const)
// The listener's own (CLOSED -> LISTEN and back) are not the connection's.
.filter(([, [from, to]]) => !(from === 'CLOSED' && to === 'LISTEN') && !(from === 'LISTEN' && to === 'CLOSED'));
const states: [number, string][] = transitions.map(([time, [, to]]) => [time, to!]);
if (!fromEstablished) return states;
const established = states.findIndex(([, state]) => state === 'ESTABLISHED');
return established < 0 ? [] : states.slice(established);
}
/** Each state Java saw comes in the engine at about the same time; the engine's others are brief. */
function expectSameStates(label: string, actual: [number, string][], expected: [number, string][]) {
let next = 0;
const matched = new Set<number>();
for (const [time, state] of expected) {
const index = actual.findIndex(([, each], candidate) => candidate >= next && each === state);
expect(index, `${label}: ${state} (Java at ${time} ms)`).toBeGreaterThanOrEqual(0);
expect(Math.abs(actual[index]![0] - time), `${label}: time of ${state}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
matched.add(index);
next = index + 1;
}
for (const [index, [time, state]] of actual.entries()) {
if (matched.has(index)) continue;
const until = actual[index + 1]?.[0] ?? Number.POSITIVE_INFINITY;
expect(until - time, `${label}: ${state} at ${time} ms, which Java did not see`).toBeLessThanOrEqual(STATE_UNSEEN_MILLISECONDS);
}
}
/** Times from the first SYN, the client's free port as "client". */
function normalized(frames: readonly SeenFrame[]): SeenFrame[] {
const syn = frames.find((frame) => / TCP .* S(A)?(F)?(P)? seq=/.test(frame.text) && !frame.text.includes(' SA'))!;
return frames.map((frame) => ({
time: frame.time - syn.time,
text: frame.text.replace(/:(\d{5})\b/g, (whole, port: string) => (Number(port) >= 49_152 ? ':client' : whole)),
}));
}
/**
* Where Java errs, per story: its run corrected, as Java means it.
*
* closedPort: connectClientMode sends the SYN in a loop over
* MAX_SENDEVERSUCHE attempts (with sendeSegment's repeat flag for the
* second and third), but its timeout branch sets closeSocket, and the loop
* ends after the first: one SYN, and verbinden fails after one wait. The
* engine sends the SYN three times, a wait of three round trips each.
*/
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
closedPort: (java) => {
const client = OTHER_CLIENT;
const syn = java.frames[client]!.find((frame) => frame.tcpFlags === 'S')!;
const waitFrom = java.states.client!.find(([, state]) => state === 'SYN_SENT')![0];
const retries = [1, 2].map((attempt) => ({ ...syn, time: waitFrom + attempt * JAVA_TIMEOUT_MILLISECONDS }));
return {
...java,
frames: { ...java.frames, [client]: [...java.frames[client]!, ...retries] },
states: {
...java.states,
client: java.states.client!.map(([time, state]) =>
state === 'CLOSED' && time > waitFrom ? [waitFrom + 3 * JAVA_TIMEOUT_MILLISECONDS, state] : [time, state],
),
},
};
},
};
describe('TCP as Java FILIUS runs it', () => {
it('corrects Java only where it errs', () => {
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `tcp-java-${name}.java.json`), 'utf8')) as JavaRun;
expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java));
}
});
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'tcp-lan.scenario.json'), 'utf8')) as Scenario;
for (const [name, story] of Object.entries(STORIES)) {
it(name, () => {
const measured = JSON.parse(readFileSync(join(scenarioDirectory, `tcp-java-${name}.java.json`), 'utf8')) as JavaRun;
const java = JAVA_BUGS[name]?.(measured) ?? measured;
const { engine, frames } = watchedEngine(scenario);
const advanceTo = (milliseconds: number) => {
engine.handle({ type: 'advance', toMilliseconds: milliseconds });
};
story.play(engine, advanceTo);
advanceTo(Math.max(...java.events.map(([time]) => time), ...Object.values(java.states).flatMap((each) => each.map(([time]) => time))) + 5_000);
const expected = normalized(javaFrames(java.frames));
const actual = normalized(frames());
const expectedSteps = gaps(expected);
const actualSteps = gaps(actual);
for (let index = 0; index < Math.max(expectedSteps.length, actualSteps.length); index += 1) {
expect(actualSteps[index]?.text, `frame #${index + 1}`).toBe(expectedSteps[index]?.text);
expect(Math.abs(actualSteps[index]!.gap - expectedSteps[index]!.gap), `gap before frame #${index + 1}`).toBeLessThanOrEqual(
GAP_TOLERANCE_MILLISECONDS,
);
}
const javaSyn = java.frames[story.client]!.find((frame) => frame.tcpFlags === 'S')!.time;
const engineSyn = frames().find((frame) => / TCP .* S seq=/.test(frame.text))!.time;
const shift = (states: [number, string][], syn: number): [number, string][] => states.map(([time, state]) => [time - syn, state]);
// From SYN_SENT: Java's first CLOSED is when the socket was made.
const javaClient = java.states.client!.filter(([, state], index) => !(index === 0 && state === 'CLOSED'));
expectSameStates('client', shift(engineStates(engine, story.client, false), engineSyn), shift(javaClient, javaSyn));
if (java.states.server !== undefined) {
expectSameStates('server', shift(engineStates(engine, SERVER, true), engineSyn), shift(java.states.server, javaSyn));
}
});
}
});
+2 -1
View File
@@ -32,7 +32,8 @@ npm test # vergleicht mit den geschriebenen *.swift.json
F2.3b folgt der DHCP-Client des Kerns Java statt Swift; die vier
DHCP-Szenarien vergleicht `dhcpJava.test.ts` mit Java-Läufen
(`tools/java-oracle`, `DhcpRun`), am Swift-Ergebnis prüft der Test nur
noch, dass sie abweichen.
noch, dass sie abweichen. Ebenso seit F2.3c die drei TCP-Szenarien
(`tcpJava.test.ts`, `TcpRun`).
Das Ergebnis enthält die Sockets mit der Länge ihrer Warteschlange, dem
TCP-Zustand, dem annehmenden Socket und den Sitzungswerten.