feat(core): programs' threads, and FILIUS' echo server and generic client after Java, measured against Java

Programs run as generators that yield what they wait for (threads.ts); the
engine resumes them after every event. TCP connect, send, receive, accept
and name resolution exist as such steps; the blocking calls wrap them.
EchoRun records eight stories in Java FILIUS, frames, tables and each
program's window lines; echoJava.test.ts plays them card by card. Not
taken from Java: a connection without a route hangs for ever; a server
switched off leaves its connections' last FIN unanswered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 16:27:44 +00:00
co-authored by Claude Opus 5.5
parent 0abce60aef
commit de3a1d4dfe
24 changed files with 13454 additions and 28 deletions
+228
View File
@@ -0,0 +1,228 @@
// Programs' threads (threads.ts) and the echo server and generic client
// (apps/echo.ts) where echoJava.test.ts's stories do not go: how threads
// wake, a taken port, a name, no route, lost data, a new start.
import { describe, expect, it } from 'vitest';
import { deviceConfiguration, pause, waitUntil, type NetworkRuntimeEngine, type Steps, type TopologySnapshotInput } from '../src/index.js';
import { link, pc, startedEngine, uuid } from './fixtures.js';
const SERVER = uuid(0x2001);
const CLIENT = uuid(0x2002);
/** 10.0.0.1 (echo server, DNS server with echo.filius.de) and 10.0.0.2 (client), on one cable. */
function network(changes: Partial<TopologySnapshotInput> = {}): TopologySnapshotInput {
return {
nodes: [pc(SERVER, uuid(0x2011)), pc(CLIENT, uuid(0x2012))],
links: [link(SERVER, uuid(0x2011), CLIENT, uuid(0x2012))],
deviceConfigurations: new Map([
[SERVER, deviceConfiguration('10.0.0.1', '255.255.255.0', '', '')],
[CLIENT, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '10.0.0.1')],
]),
echoServersByNodeId: new Map([[SERVER, { port: 55555, isActive: true }]]),
simpleClientsByNodeId: new Map([[CLIENT, { targetAddress: '', targetPort: 55555 }]]),
...changes,
};
}
const advance = (engine: NetworkRuntimeEngine, toMilliseconds: number) => engine.handle({ type: 'advance', toMilliseconds });
const kinds = (lines: readonly { kind: string }[]) => lines.map((line) => line.kind);
describe("programs' threads", () => {
it('wake when their condition holds, right after the event that made it so', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
let flag = false;
const woken: number[] = [];
engine.startThread(
(function* (): Steps {
yield* waitUntil(() => flag);
woken.push(engine.state.currentTimeMilliseconds);
})(),
);
engine.handle({ type: 'schedule', deadlineMilliseconds: 300, kind: { kind: 'parityMarker', label: 'a' } });
advance(engine, 200);
flag = true;
expect(woken).toEqual([]);
advance(engine, 1_000);
expect(woken).toEqual([300]);
});
it('go on after a pause, after the threads that are ready at that time', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
const order: string[] = [];
let ready = false;
engine.startThread(
(function* (): Steps {
yield pause(engine.state.currentTimeMilliseconds);
order.push(`paused ${engine.state.currentTimeMilliseconds}`);
})(),
);
engine.startThread(
(function* (): Steps {
yield* waitUntil(() => ready);
order.push(`waited ${engine.state.currentTimeMilliseconds}`);
})(),
);
ready = true;
engine.resumeThreads();
advance(engine, 0);
expect(order).toEqual(['waited 0', 'paused 0']);
});
it('wake from a wait by its time only once, not again in a later wait', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
let flag = false;
const woken: string[] = [];
engine.startThread(
(function* (): Steps {
const first = yield* waitUntil(() => flag, 500);
woken.push(`first ${first} ${engine.state.currentTimeMilliseconds}`);
const second = yield* waitUntil(() => false, 2_000);
woken.push(`second ${second} ${engine.state.currentTimeMilliseconds}`);
})(),
);
engine.handle({ type: 'schedule', deadlineMilliseconds: 100, kind: { kind: 'parityMarker', label: 'a' } });
flag = true;
advance(engine, 3_000);
// The first wait ended at 100 by its condition; its wake-up at 500 does not end the second.
expect(woken).toEqual(['first true 100', 'second false 2000']);
});
it('resume in the order they began when the same event wakes them', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
let flag = false;
const order: string[] = [];
for (const name of ['first', 'second', 'third']) {
engine.startThread(
(function* (): Steps {
yield* waitUntil(() => flag);
order.push(name);
})(),
);
}
flag = true;
engine.resumeThreads();
expect(order).toEqual(['first', 'second', 'third']);
});
it('do not wait for what already holds: no other thread runs in between', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
let flag = false;
const order: string[] = [];
engine.startThread(
(function* (): Steps {
yield* waitUntil(() => flag);
order.push('woken');
})(),
);
engine.startThread(
(function* (): Steps {
flag = true;
yield* waitUntil(() => true);
order.push('went on');
})(),
);
expect(order).toEqual(['went on', 'woken']);
});
it('end with the simulation', () => {
const engine = startedEngine(network());
expect(engine.echoServer(SERVER)).toBeDefined();
engine.handle({ type: 'stop' });
expect(engine.echoServer(SERVER)).toBeUndefined();
expect(engine.simpleClient(CLIENT)).toBeUndefined();
});
});
describe('the echo server and the generic client', () => {
it('switches the server off when its port is taken (sw_serveranwendung_msg6)', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map([[SERVER, { port: 7, isActive: false }]]) }));
expect(engine.openTcpServerSocket(SERVER, 7)).toBeDefined();
const server = engine.echoServer(SERVER)!;
server.setActive(true);
advance(engine, 100);
expect(kinds(server.log.all)).toEqual(['acceptingStarted', 'serverSocketFailed', 'acceptingStopped']);
expect(server.isActive).toBe(false);
});
it('connects to a name through the DNS server', () => {
const engine = startedEngine(
network({
dnsServerConfigurationsByNodeId: new Map([[SERVER, { recursiveResolutionEnabled: false, records: [{ name: 'echo.filius.de', type: 'A', ttlSeconds: 3600, target: '10.0.0.1' }] }]]),
}),
);
const client = engine.simpleClient(CLIENT)!;
client.connect('echo.filius.de', 55555);
advance(engine, 3_000);
client.send('Hallo');
advance(engine, 6_000);
expect(kinds(client.log.all)).toEqual(['connected', 'sent', 'received']);
expect(engine.echoServer(SERVER)!.log.all[0]).toMatchObject({ kind: 'connectionAccepted', address: '10.0.0.2' });
});
it('fails to connect to a name no server knows, without a frame (sw_socket_msg1)', () => {
const engine = startedEngine(network({ deviceConfigurations: new Map([[CLIENT, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '')]]) }));
const client = engine.simpleClient(CLIENT)!;
client.connect('echo.filius.de', 55555);
advance(engine, 5_000);
expect(client.log.all).toEqual([{ kind: 'connectFailed', failure: 'unresolved', timeMilliseconds: 0 }]);
expect(engine.capture.frames(uuid(0x2012))).toEqual([]);
});
it('fails at once to connect to an address without a route, without a frame (Java waits for ever)', () => {
const engine = startedEngine(network());
const client = engine.simpleClient(CLIENT)!;
advance(engine, 100);
client.connect('10.9.9.9', 55555);
expect(client.log.all).toEqual([{ kind: 'connectFailed', failure: 'failed', timeMilliseconds: 100 }]);
advance(engine, 5_000);
expect(engine.capture.frames(uuid(0x2012))).toEqual([]);
});
it('reports a send that no one acknowledges and the connection it ends (sw_tcpsocket_msg8)', () => {
const engine = startedEngine(network());
const client = engine.simpleClient(CLIENT)!;
client.connect('10.0.0.1', 55555);
advance(engine, 3_000);
engine.setGlobalPacketLossEnabled(true);
client.send('verloren');
advance(engine, 40_000);
expect(client.log.all.filter((line) => line.kind === 'sendFailed')).toMatchObject([{ kind: 'sendFailed', failure: 'timedOut' }]);
expect(kinds(client.log.all)).toContain('connectionClosed');
expect(client.isConnected).toBe(false);
});
it('starts anew with a new simulation: from the snapshot, with empty windows', () => {
const engine = startedEngine(network());
engine.echoServer(SERVER)!.setActive(false);
engine.simpleClient(CLIENT)!.send('Hallo');
engine.handle({ type: 'stop' });
engine.handle({ type: 'start', snapshot: engine.state.topologySnapshot, seed: 14, initialTimeMilliseconds: 0 });
expect(engine.echoServer(SERVER)!.isActive).toBe(true);
expect(engine.echoServer(SERVER)!.log.all).toEqual([]);
expect(engine.simpleClient(CLIENT)!.log.all).toEqual([]);
});
it('hands out a message that came before the connection closed (Java drops it)', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
const listener = engine.openTcpServerSocket(SERVER, 80)!;
const client = engine.openTcpClientSocket(CLIENT, '10.0.0.1', 80)!;
expect(engine.connectTcpWithResult(client)).toBe('connected');
const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listener)![0]!;
advance(engine, engine.state.currentTimeMilliseconds + 100);
expect(engine.sendTcp(accepted, new TextEncoder().encode('zuletzt'))).toBe(true);
engine.closeTcpSocket(accepted);
advance(engine, engine.state.currentTimeMilliseconds + 1_000);
expect(engine.state.socketsById.get(client)!.tcpState).not.toBe('ESTABLISHED');
const received: string[] = [];
engine.startThread(
(function* (): Steps {
for (;;) {
const next = yield* engine.receiveTcpSteps(client);
received.push(next.type === 'message' ? new TextDecoder().decode(next.payload) : next.type);
if (next.type !== 'message') return;
}
})(),
);
expect(received).toEqual(['zuletzt', 'notConnected']);
});
});
+337
View File
@@ -0,0 +1,337 @@
// The echo server and the generic client as Java FILIUS runs them.
// tools/java-oracle's EchoRun played stories of the two programs on tcp-lan's
// network in Java FILIUS and wrote every frame each card saw, Java's tables
// of them, and every line each program wrote into its window
// (echo-java-<story>.java.json). The engine plays the same stories with its
// programs (apps/echo.ts): each card's frames must come in the same order,
// with the same content and rows, at about the same time; each program's
// lines must read as Java's (its German message bundle), in the same order,
// at about the same time.
//
// Java's client looks every 100 ms whether it is connected
// (TCPSocket.verbinden), its server likewise whether a connection came
// (ServerSocket.oeffnen) or it was switched off; the engine at once. Those
// lines come in Java up to POLL_MILLISECONDS later.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { withSnapshot, type EchoServerLogEntry, type LogLine, type NetworkRuntimeEngine, type SendFailure, type SimpleClientLogEntry } from '../src/index.js';
import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, withClientPortText, type JavaFrame, type JavaTables } from './javaRun.js';
import { snapshotOf, type Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const SERVER = '00000000-0000-0000-0000-000000000101';
const CLIENT = '00000000-0000-0000-0000-000000000102';
const OTHER_CLIENT = '00000000-0000-0000-0000-000000000103';
const POLL_MILLISECONDS = 100;
type Program = 'server' | 'client' | 'other';
interface JavaRun extends JavaTables {
frames: Record<string, JavaFrame[]>;
/** Time, program, and a line of its window or one of the story's calls ("call …", "returned …"). */
events: [number, Program, string][];
}
/** sw_tcpsocket_msg6, msg7, msg8. */
const SEND_FAILURES: Record<SendFailure, string> = {
notConnected: 'Verbindung nicht aufgebaut!',
connectionLost: 'Verbindung wurde unterbrochen',
timedOut: 'Zeitüberschreitung während des Versands einer Nachricht',
};
/** A server's line as Java's window shows it (German message bundle). */
function serverText(entry: EchoServerLogEntry): string {
switch (entry.kind) {
case 'acceptingStarted':
return 'Annahme von Verbindungsanfragen gestartet';
case 'acceptingStopped':
return 'Annahme von Verbindungsanfragen beendet';
case 'serverSocketClosed':
return 'Server-Socket geschlossen';
case 'serverSocketFailed':
return 'Server-Socket konnte nicht geöffnet werden.';
case 'connectionAccepted':
return `Verbindung zu ${entry.address}:${entry.port} hergestellt`;
case 'received':
return `>>${entry.text}`;
case 'sent':
return `<<${entry.text}`;
case 'sendFailed':
return SEND_FAILURES[entry.failure];
case 'connectionClosed':
return `Socket zu ${entry.address}:${entry.port} geschlossen`;
}
}
const CONNECT_FAILURES = {
unresolved: 'Ziel konnte nicht bestimmt werden!',
timedOut: 'Zeitüberschreitung beim Verbindungsaufbau',
failed: 'Aktives Öffnen des Sockets fehlgeschlagen',
};
/** A client's line as Java's window shows it. */
function clientText(entry: SimpleClientLogEntry): string {
switch (entry.kind) {
case 'connected':
return 'Verbindung hergestellt';
case 'connectFailed':
return `Fehler beim Verbindungsaufbau\n${CONNECT_FAILURES[entry.failure]}`;
case 'disconnected':
return 'Verbindung getrennt';
case 'notConnected':
return 'Fehler beim Verbindungsaufbau';
case 'sent':
return `<<${entry.text}`;
case 'received':
return `>>${entry.text}`;
case 'sendFailed':
return SEND_FAILURES[entry.failure];
case 'connectionClosed':
return `Socket zu ${entry.address}:${entry.port} getrennt`;
case 'receiveFailed':
return 'Keine Verbindung hergestellt!';
}
}
/** Lines Java writes after one of its 100 ms looks. */
const POLLED = new Set(['Verbindung hergestellt', 'Server-Socket geschlossen']);
const isPolled = (text: string) => POLLED.has(text) || /^Verbindung zu .* hergestellt$/.test(text);
type Act = (engine: NetworkRuntimeEngine) => void;
const server = (engine: NetworkRuntimeEngine) => engine.echoServer(SERVER)!;
const client = (engine: NetworkRuntimeEngine) => engine.simpleClient(CLIENT)!;
const other = (engine: NetworkRuntimeEngine) => engine.simpleClient(OTHER_CLIENT)!;
/** Each story's calls at their times, as EchoRun makes them. */
const STORIES: Record<string, [number, Act][]> = {
exchange: [
[500, (engine) => client(engine).connect('192.168.0.10', 55555)],
[2000, (engine) => client(engine).send('Hallo')],
[4000, (engine) => client(engine).send('Welt')],
[6000, (engine) => client(engine).disconnect()],
],
closedPort: [[500, (engine) => client(engine).connect('192.168.0.30', 55555)]],
noRoute: [[500, (engine) => client(engine).connect('10.0.0.1', 55555)]],
notConnected: [[500, (engine) => client(engine).send('Hallo')]],
serverOff: [
[500, (engine) => client(engine).connect('192.168.0.10', 55555)],
[2000, (engine) => server(engine).setActive(false)],
[4000, (engine) => client(engine).send('Hallo')],
[6000, (engine) => server(engine).setActive(true)],
[7000, (engine) => client(engine).connect('192.168.0.10', 55555)],
[9000, (engine) => client(engine).send('Welt')],
],
reconnect: [
[500, (engine) => client(engine).connect('192.168.0.10', 55555)],
[2000, (engine) => client(engine).disconnect()],
[4000, (engine) => client(engine).connect('192.168.0.10', 55555)],
[6000, (engine) => client(engine).send('Hallo')],
],
twoClients: [
[500, (engine) => client(engine).connect('192.168.0.10', 55555)],
[1000, (engine) => other(engine).connect('192.168.0.10', 55555)],
[2000, (engine) => client(engine).send('eins')],
[3000, (engine) => other(engine).send('zwei')],
[4000, (engine) => client(engine).disconnect()],
[5000, (engine) => other(engine).disconnect()],
],
otherPort: [
[500, (engine) => server(engine).setActive(false)],
[
1500,
(engine) => {
server(engine).setPort(7);
server(engine).setActive(true);
},
],
[2000, (engine) => client(engine).connect('192.168.0.10', 7)],
[4000, (engine) => client(engine).send('Hallo')],
[6000, (engine) => other(engine).connect('192.168.0.10', 55555)],
],
};
/** The windows' lines, time and text, per program. */
type Lines = Record<Program, [number, string][]>;
function javaLines(java: JavaRun): Lines {
const lines: Lines = { server: [], client: [], other: [] };
for (const [time, program, text] of java.events) {
// Anwendung.starten tells its observers nothing (null), which the window skips.
if (text === 'null' || text.startsWith('call ') || text.startsWith('returned ')) continue;
lines[program].push([time, withClientPortText(text)]);
}
return lines;
}
function engineLines(engine: NetworkRuntimeEngine): Lines {
const lines = <E>(log: readonly LogLine<E>[], text: (entry: E) => string): [number, string][] =>
log.map((line) => [line.timeMilliseconds, withClientPortText(text(line))]);
return {
server: lines(server(engine).log.all, serverText),
client: lines(client(engine).log.all, clientText),
other: lines(other(engine).log.all, clientText),
};
}
/** Java's timeout of a TCP attempt (3 round trips at 90 %), and the round trip of a frame between two cards on the switch. */
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
const CARD_TO_CARD_MILLISECONDS = 111;
/** NumberFormat for Locale.GERMANY. */
const javaNumber = (value: number) => String(value).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
/** Java's rows of a TCP segment without data (Lauscher.datenVorbereiten), with addresses or ports only. */
function tcpRows(number: number, frame: JavaFrame, withAddresses: boolean): (string | null)[][] {
const end = (address: string, port: number) => (withAddresses ? `${address}:${port}` : String(port));
return [
[String(number), frame.source, frame.destination, '', 'Netzzugang', '0x800'],
[String(number), frame.sourceIp!, frame.destinationIp!, 'IP', 'Vermittlung', 'Protokoll: 6, TTL: 64'],
[
String(number),
end(frame.sourceIp!, frame.sourcePort!),
end(frame.destinationIp!, frame.destinationPort!),
'TCP',
'Transport',
`SEQ: ${javaNumber(frame.sequence!)}, ACK: ${javaNumber(frame.acknowledgement!)}`,
],
];
}
/** Java's run with a frame more on a card, in time order, its rows numbered in. */
function withJavaFrame(java: JavaRun, card: string, frame: JavaFrame): JavaRun {
const frames = java.frames[card]!;
const index = frames.filter((each) => each.time <= frame.time).length;
const renumbered = (rows: (string | null)[][], withAddresses: boolean) => [
...rows.map((row) => (Number(row[0]) > index ? [String(Number(row[0]) + 1), ...row.slice(1)] : row)),
...tcpRows(index + 1, frame, withAddresses),
];
return {
...java,
frames: { ...java.frames, [card]: [...frames.slice(0, index), frame, ...frames.slice(index)] },
rows: { ...java.rows, [card]: renumbered(java.rows![card]!, true) },
layeredRows: { ...java.layeredRows, [card]: renumbered(java.layeredRows![card]!, false) },
};
}
/** When the program last called verbinden: its SYN goes down then, and the attempt's time runs from there. */
const lastConnect = (java: JavaRun, program: Program) => java.events.filter(([, who, text]) => who === program && text.startsWith('call verbinden')).at(-1)![0];
/** Java's connection attempt with one SYN: its failure two attempts later, when the third has timed out. */
function failingAfterThreeAttempts(java: JavaRun, program: Program): JavaRun {
return {
...java,
events: java.events.map(([time, who, text]) =>
who === program && text.startsWith('Fehler beim Verbindungsaufbau\n') ? [time + 2 * JAVA_TIMEOUT_MILLISECONDS, who, text] : [time, who, text],
),
};
}
/**
* Where Java errs, per story: its run corrected, as Java means it.
*
* noRoute: the client's socket finds no route for its SYN; the exception
* ends the socket's thread, and verbinden waits for ever for a state that
* never comes — the client hangs without a word (and is no longer usable).
* The engine's connection fails at once (sw_tcpsocket_msg3).
*
* closedPort, otherPort: Java's client sends one SYN, not three
* (tcpJava.test.ts, closedPort); its connection fails two attempts later in
* the engine.
*
* serverOff: switched off, Java's server frees its port (ServerSocket
* .schliessen) while its connections still close; the client's FIN finds
* no socket and goes unanswered, the client waits in LAST_ACK in vain. The
* engine's server answers it with its ACK.
*/
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
noRoute: (java) => ({
...java,
events: [...java.events, [java.events.find(([, , text]) => text.startsWith('call verbinden'))![0], 'client', `Fehler beim Verbindungsaufbau\n${CONNECT_FAILURES.failed}`]],
}),
closedPort: (java) => failingAfterThreeAttempts(java, 'client'),
otherPort: (java) => failingAfterThreeAttempts(java, 'other'),
serverOff: (java) => {
const fin = java.frames[SERVER]!.filter((frame) => frame.tcpFlags === 'F').at(-1)!;
const ack: JavaFrame = {
...fin,
source: fin.destination,
destination: fin.source,
sourceIp: fin.destinationIp!,
destinationIp: fin.sourceIp!,
sourcePort: fin.destinationPort!,
destinationPort: fin.sourcePort!,
tcpFlags: 'A',
sequence: java.frames[SERVER]!.find((frame) => frame.tcpFlags === 'F')!.sequence! + 1,
acknowledgement: fin.sequence! + 1,
};
return withJavaFrame(withJavaFrame(java, SERVER, ack), CLIENT, { ...ack, time: fin.time + CARD_TO_CARD_MILLISECONDS });
},
};
/**
* Where each card's capture is compared only up to a time (Java's).
* closedPort, otherPort: the engine's second and third SYN come when the
* switch has forgotten the server (tcp-lan: 5 s) and floods them to every
* card; compared up to the second.
*/
const CAPTURE_UNTIL: Record<string, (java: JavaRun) => number> = {
closedPort: (java) => lastConnect(java, 'client') + JAVA_TIMEOUT_MILLISECONDS - GAP_TOLERANCE_MILLISECONDS,
otherPort: (java) => lastConnect(java, 'other') + JAVA_TIMEOUT_MILLISECONDS - GAP_TOLERANCE_MILLISECONDS,
};
describe('the echo server and the generic client as Java FILIUS runs them', () => {
it('cuts only where the engine goes on beyond Java', () => {
for (const [name, until] of Object.entries(CAPTURE_UNTIL)) {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `echo-java-${name}.java.json`), 'utf8')) as JavaRun;
const cut = until(java);
expect(Object.values(java.frames).flat().every((frame) => frame.time < cut), name).toBe(true);
}
});
it('corrects Java only where it errs', () => {
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `echo-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;
const snapshot = withSnapshot(snapshotOf(scenario), {
echoServersByNodeId: new Map([[SERVER, { port: 55555, isActive: true }]]),
simpleClientsByNodeId: new Map([
[CLIENT, { targetAddress: '', targetPort: 55555 }],
[OTHER_CLIENT, { targetAddress: '', targetPort: 55555 }],
]),
});
for (const [name, acts] of Object.entries(STORIES)) {
it(name, () => {
const measured = JSON.parse(readFileSync(join(scenarioDirectory, `echo-java-${name}.java.json`), 'utf8')) as JavaRun;
const java = JAVA_BUGS[name]?.(measured) ?? measured;
const { engine } = watchedEngine(scenario, snapshot);
for (const [time, act] of acts) {
engine.handle({ type: 'advance', toMilliseconds: time });
act(engine);
}
engine.handle({ type: 'advance', toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 5_000 });
expectSameCaptures(engine, scenario, java.frames, { text: withClientPortText, until: CAPTURE_UNTIL[name]?.(measured), tables: java });
const expected = javaLines(java);
const actual = engineLines(engine);
for (const program of ['server', 'client', 'other'] as const) {
expect(actual[program].map(([, text]) => text), `${program}'s lines`).toEqual(expected[program].map(([, text]) => text));
for (const [index, [javaTime, text]] of expected[program].entries()) {
const late = javaTime - actual[program][index]![0];
const [least, most] = isPolled(text) ? [-GAP_TOLERANCE_MILLISECONDS, POLL_MILLISECONDS + GAP_TOLERANCE_MILLISECONDS] : [-GAP_TOLERANCE_MILLISECONDS, GAP_TOLERANCE_MILLISECONDS];
expect(late, `${program}'s line ${JSON.stringify(text)}: Java ${late} ms later`).toBeGreaterThanOrEqual(least);
expect(late, `${program}'s line ${JSON.stringify(text)}: Java ${late} ms later`).toBeLessThanOrEqual(most);
}
}
});
}
});
+7 -3
View File
@@ -30,6 +30,7 @@ import {
type CapturedFrame,
type CaptureLayer,
type EthernetFrame,
type TopologySnapshot,
} from '../src/index.js';
import { snapshotOf, type Scenario } from './scenario.js';
@@ -117,8 +118,11 @@ export function engineText(frame: EthernetFrame): string {
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) {
/**
* A started engine on the scenario's network (or on `snapshot`, made from
* it) whose frames can be read afterwards as FILIUS logs them.
*/
export function watchedEngine(scenario: Scenario, snapshot: TopologySnapshot = snapshotOf(scenario)) {
const engine = new NetworkRuntimeEngine(scenario.seed);
const framesByIdentity = new Map<number, EthernetFrame>();
const send = engine.sendEthernetFrame.bind(engine);
@@ -126,7 +130,7 @@ export function watchedEngine(scenario: Scenario) {
framesByIdentity.set(frame.identity, frame);
send(fromNodeId, outgoingPortId, frame);
};
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
engine.handle({ type: 'start', snapshot, seed: scenario.seed, initialTimeMilliseconds: 0 });
/** Every frame when it first shows on a host's card (sent, or received). */
const frames = (): SeenFrame[] => {
@@ -0,0 +1,377 @@
{
"frames": {
"00000000-0000-0000-0000-000000000101": [
{
"time": 641,
"source": "02:00:00:00:01:12",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.30"
}
],
"00000000-0000-0000-0000-000000000102": [
{
"time": 519,
"source": "02:00:00:00:01:12",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.30"
},
{
"time": 755,
"source": "02:00:00:00:01:13",
"destination": "02:00:00:00:01:12",
"arp": 2,
"senderIp": "192.168.0.30",
"targetIp": "192.168.0.20"
},
{
"time": 758,
"source": "02:00:00:00:01:12",
"destination": "02:00:00:00:01:13",
"sourceIp": "192.168.0.20",
"destinationIp": "192.168.0.30",
"protocol": 6,
"sourcePort": 58845,
"destinationPort": 55555,
"payload": null,
"tcpFlags": "S",
"sequence": 2000000,
"acknowledgement": 0
}
],
"00000000-0000-0000-0000-000000000103": [
{
"time": 641,
"source": "02:00:00:00:01:12",
"destination": "FF:FF:FF:FF:FF:FF",
"arp": 1,
"senderIp": "192.168.0.20",
"targetIp": "192.168.0.30"
},
{
"time": 643,
"source": "02:00:00:00:01:13",
"destination": "02:00:00:00:01:12",
"arp": 2,
"senderIp": "192.168.0.30",
"targetIp": "192.168.0.20"
},
{
"time": 871,
"source": "02:00:00:00:01:12",
"destination": "02:00:00:00:01:13",
"sourceIp": "192.168.0.20",
"destinationIp": "192.168.0.30",
"protocol": 6,
"sourcePort": 58845,
"destinationPort": 55555,
"payload": null,
"tcpFlags": "S",
"sequence": 2000000,
"acknowledgement": 0
}
]
},
"rows": {
"00000000-0000-0000-0000-000000000101": [
[
"1",
"02:00:00:00:01:12",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.30",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]"
]
],
"00000000-0000-0000-0000-000000000102": [
[
"1",
"02:00:00:00:01:12",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.30",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]"
],
[
"2",
"02:00:00:00:01:13",
"02:00:00:00:01:12",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.30",
"192.168.0.20",
"ARP",
"Vermittlung",
"MAC ist 02:00:00:00:01:13 [op=REPLY, sender=02:00:00:00:01:13|192.168.0.30, target=02:00:00:00:01:12|192.168.0.20]"
],
[
"3",
"02:00:00:00:01:12",
"02:00:00:00:01:13",
"",
"Netzzugang",
"0x800"
],
[
"3",
"192.168.0.20",
"192.168.0.30",
"IP",
"Vermittlung",
"Protokoll: 6, TTL: 64"
],
[
"3",
"192.168.0.20:58845",
"192.168.0.30:55555",
"TCP",
"Transport",
"SYN, SEQ: 2.000.000"
]
],
"00000000-0000-0000-0000-000000000103": [
[
"1",
"02:00:00:00:01:12",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.30",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]"
],
[
"2",
"02:00:00:00:01:13",
"02:00:00:00:01:12",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.30",
"192.168.0.20",
"ARP",
"Vermittlung",
"MAC ist 02:00:00:00:01:13 [op=REPLY, sender=02:00:00:00:01:13|192.168.0.30, target=02:00:00:00:01:12|192.168.0.20]"
],
[
"3",
"02:00:00:00:01:12",
"02:00:00:00:01:13",
"",
"Netzzugang",
"0x800"
],
[
"3",
"192.168.0.20",
"192.168.0.30",
"IP",
"Vermittlung",
"Protokoll: 6, TTL: 64"
],
[
"3",
"192.168.0.20:58845",
"192.168.0.30:55555",
"TCP",
"Transport",
"SYN, SEQ: 2.000.000"
]
]
},
"layeredRows": {
"00000000-0000-0000-0000-000000000101": [
[
"1",
"02:00:00:00:01:12",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.30",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]"
]
],
"00000000-0000-0000-0000-000000000102": [
[
"1",
"02:00:00:00:01:12",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.30",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]"
],
[
"2",
"02:00:00:00:01:13",
"02:00:00:00:01:12",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.30",
"192.168.0.20",
"ARP",
"Vermittlung",
"MAC ist 02:00:00:00:01:13 [op=REPLY, sender=02:00:00:00:01:13|192.168.0.30, target=02:00:00:00:01:12|192.168.0.20]"
],
[
"3",
"02:00:00:00:01:12",
"02:00:00:00:01:13",
"",
"Netzzugang",
"0x800"
],
[
"3",
"192.168.0.20",
"192.168.0.30",
"IP",
"Vermittlung",
"Protokoll: 6, TTL: 64"
],
[
"3",
"58845",
"55555",
"TCP",
"Transport",
"SYN, SEQ: 2.000.000"
]
],
"00000000-0000-0000-0000-000000000103": [
[
"1",
"02:00:00:00:01:12",
"FF:FF:FF:FF:FF:FF",
"",
"Netzzugang",
"0x806"
],
[
"1",
"192.168.0.20",
"192.168.0.30",
"ARP",
"Vermittlung",
"Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]"
],
[
"2",
"02:00:00:00:01:13",
"02:00:00:00:01:12",
"",
"Netzzugang",
"0x806"
],
[
"2",
"192.168.0.30",
"192.168.0.20",
"ARP",
"Vermittlung",
"MAC ist 02:00:00:00:01:13 [op=REPLY, sender=02:00:00:00:01:13|192.168.0.30, target=02:00:00:00:01:12|192.168.0.20]"
],
[
"3",
"02:00:00:00:01:12",
"02:00:00:00:01:13",
"",
"Netzzugang",
"0x800"
],
[
"3",
"192.168.0.20",
"192.168.0.30",
"IP",
"Vermittlung",
"Protokoll: 6, TTL: 64"
],
[
"3",
"58845",
"55555",
"TCP",
"Transport",
"SYN, SEQ: 2.000.000"
]
]
},
"events": [
[
211,
"server",
"null"
],
[
212,
"client",
"null"
],
[
213,
"other",
"null"
],
[
500,
"client",
"call verbinden 192.168.0.30:55555"
],
[
8810,
"client",
"Fehler beim Verbindungsaufbau\nZeitüberschreitung beim Verbindungsaufbau"
]
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
{
"frames": {
"00000000-0000-0000-0000-000000000101": [],
"00000000-0000-0000-0000-000000000102": [],
"00000000-0000-0000-0000-000000000103": []
},
"rows": {
"00000000-0000-0000-0000-000000000101": [],
"00000000-0000-0000-0000-000000000102": [],
"00000000-0000-0000-0000-000000000103": []
},
"layeredRows": {
"00000000-0000-0000-0000-000000000101": [],
"00000000-0000-0000-0000-000000000102": [],
"00000000-0000-0000-0000-000000000103": []
},
"events": [
[
210,
"server",
"null"
],
[
211,
"client",
"null"
],
[
211,
"other",
"null"
],
[
501,
"client",
"call verbinden 10.0.0.1:55555"
]
]
}
@@ -0,0 +1,49 @@
{
"frames": {
"00000000-0000-0000-0000-000000000101": [],
"00000000-0000-0000-0000-000000000102": [],
"00000000-0000-0000-0000-000000000103": []
},
"rows": {
"00000000-0000-0000-0000-000000000101": [],
"00000000-0000-0000-0000-000000000102": [],
"00000000-0000-0000-0000-000000000103": []
},
"layeredRows": {
"00000000-0000-0000-0000-000000000101": [],
"00000000-0000-0000-0000-000000000102": [],
"00000000-0000-0000-0000-000000000103": []
},
"events": [
[
208,
"server",
"null"
],
[
209,
"client",
"null"
],
[
210,
"other",
"null"
],
[
500,
"client",
"call senden Hallo"
],
[
500,
"client",
"Fehler beim Verbindungsaufbau"
],
[
500,
"client",
"returned senden"
]
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff