feat(core): sends return at once and go through Java's transport thread
A send hands its frame to the card and returns, as in Java; the frames travel while the clock advances. Swift's send ran the network until every frame had arrived; the option swiftSends keeps that for the Swift comparison. Waiting calls (TCP connect, acknowledgement) now run event by event until the answer instead of to the timeout. UDP and TCP send through a thread per node and protocol (Java's TransportProtokoll): one segment after the other; while IP waits for ARP, the next send blocks and a segment from an event queues behind it. Senders that wait take their turns in the order they came (a fair lock; Java's leaves the order to chance). All four ArpRun stories now match Java FILIUS, including when each call returns. Tests let frames arrive explicitly with letFramesArrive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
// card saw and when each call started and ended (arp-java-<story>.java.json).
|
||||
// The engine makes the same calls: the frames must come in the same order,
|
||||
// with the same content, at about the same distance from each other
|
||||
// (javaRun.ts), the calls must return the same, after about as long.
|
||||
// (javaRun.ts); the calls must end at about the same times and return the same.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
@@ -27,28 +27,41 @@ function pingResult(result: IPv4DeliveryResult, answered: boolean): string {
|
||||
return answered ? 'ICMP 0/0' : 'null';
|
||||
}
|
||||
|
||||
const STORIES: Record<string, (engine: NetworkRuntimeEngine) => string> = {
|
||||
pingLan: (engine) => pingResult(engine.sendIcmpEcho(HOST, '10.0.0.9'), false),
|
||||
pingRouted: (engine) => {
|
||||
const result = engine.sendIcmpEcho(HOST, '20.0.0.9');
|
||||
return pingResult(result, result.type === 'delivered' && result.nodeId !== HOST);
|
||||
/** Records a call as ArpRun does: when it started, when it ended and what it returned. */
|
||||
type Call = (label: string, run: () => string) => void;
|
||||
|
||||
function udpSend(engine: NetworkRuntimeEngine, target: string, payload: string): string {
|
||||
const socket = engine.bindUdpSocket(HOST, { remoteIPAddress: target, remotePort: 9 })!;
|
||||
engine.sendUdp(socket, new TextEncoder().encode(payload));
|
||||
return '';
|
||||
}
|
||||
|
||||
const STORIES: Record<string, (engine: NetworkRuntimeEngine, call: Call) => void> = {
|
||||
pingLan: (engine, call) => call('sendProbe 10.0.0.9', () => pingResult(engine.sendIcmpEcho(HOST, '10.0.0.9'), false)),
|
||||
pingRouted: (engine, call) =>
|
||||
call('sendProbe 20.0.0.9', () => {
|
||||
const result = engine.sendIcmpEcho(HOST, '20.0.0.9');
|
||||
return pingResult(result, result.type === 'delivered' && result.nodeId !== HOST);
|
||||
}),
|
||||
udpTwice: (engine, call) => {
|
||||
call('senden eins', () => udpSend(engine, '10.0.0.9', 'eins'));
|
||||
call('senden zwei', () => udpSend(engine, '10.0.0.9', 'zwei'));
|
||||
},
|
||||
udpBehind: (engine, call) => {
|
||||
call('senden verloren', () => udpSend(engine, '10.0.0.9', 'verloren'));
|
||||
call('senden dahinter', () => udpSend(engine, '20.0.0.2', 'dahinter'));
|
||||
},
|
||||
udpTwice: () => '',
|
||||
udpBehind: () => '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Stories the engine does not play like Java yet, with the reason; each must
|
||||
* still differ from Java.
|
||||
*
|
||||
* udpTwice, udpBehind: Java's transport thread sends one segment after the
|
||||
* other and holds its queue while ARP waits, so the next send blocks — the
|
||||
* engine's threads come in the next step.
|
||||
*/
|
||||
const NOT_YET_LIKE_JAVA: Record<string, string> = {
|
||||
udpTwice: "Java's transport thread: the second datagram waits for the first one's ARP",
|
||||
udpBehind: "Java's transport thread: a datagram to a reachable host waits behind one whose ARP fails",
|
||||
};
|
||||
/** The client's free port as "client": Java draws it at random, the engine from its seed. */
|
||||
function withClientPort<T extends { text: string }>(frames: readonly T[]): T[] {
|
||||
return frames.map((frame) => ({ ...frame, text: frame.text.replace(/:(\d{5})\b/g, (whole, port: string) => (Number(port) >= 49_152 ? ':client' : whole)) }));
|
||||
}
|
||||
|
||||
/** Java's events without its answer's sender ("from …"), which the engine's results do not name. */
|
||||
function javaEvent(event: string): string {
|
||||
return event.replace(/ from .*$/, '');
|
||||
}
|
||||
|
||||
describe('ARP as Java FILIUS runs it', () => {
|
||||
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'arp-routed.scenario.json'), 'utf8')) as Scenario;
|
||||
@@ -58,20 +71,22 @@ describe('ARP as Java FILIUS runs it', () => {
|
||||
const java = JSON.parse(readFileSync(join(scenarioDirectory, `arp-java-${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const { engine, frames } = watchedEngine(scenario);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 500 });
|
||||
const called = engine.state.currentTimeMilliseconds;
|
||||
const result = story(engine);
|
||||
const returned = engine.state.currentTimeMilliseconds;
|
||||
const events: [number, string][] = [];
|
||||
story(engine, (label, run) => {
|
||||
events.push([engine.state.currentTimeMilliseconds, `${label} called`]);
|
||||
const result = run();
|
||||
events.push([engine.state.currentTimeMilliseconds, `${label} returned ${result}`]);
|
||||
});
|
||||
engine.handle({ type: 'advance', toMilliseconds: 16_000 });
|
||||
|
||||
const expected = gaps(javaFrames(java.frames));
|
||||
const actual = gaps(frames());
|
||||
if (name in NOT_YET_LIKE_JAVA) {
|
||||
expect(JSON.stringify(actual), NOT_YET_LIKE_JAVA[name]).not.toBe(JSON.stringify(expected));
|
||||
return;
|
||||
const expected = gaps(withClientPort(javaFrames(java.frames)));
|
||||
const actual = gaps(withClientPort(frames()));
|
||||
expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => javaEvent(event)));
|
||||
for (const [index, [time]] of events.entries()) {
|
||||
expect(Math.abs(time - events[0]![0] - (java.events[index]![0] - java.events[0]![0])), `time of ${events[index]![1]}`).toBeLessThanOrEqual(
|
||||
GAP_TOLERANCE_MILLISECONDS,
|
||||
);
|
||||
}
|
||||
const [[javaCalled], [javaReturned, javaOutcome]] = java.events as [[number, string], [number, string]];
|
||||
expect(javaOutcome.replace(/^.* returned /, '').replace(/ from .*$/, '')).toBe(result);
|
||||
expect(Math.abs(returned - called - (javaReturned - javaCalled)), 'duration of the call').toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
|
||||
for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) {
|
||||
expect(actual[index]?.text, `frame #${index + 1}`).toBe(expected[index]?.text);
|
||||
expect(Math.abs(actual[index]!.gap - expected[index]!.gap), `gap before frame #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
|
||||
|
||||
Reference in New Issue
Block a user