// ARP as Java FILIUS runs it. tools/java-oracle's ArpRun sent to addresses no // card has on arp-routed's network in Java FILIUS and wrote every frame each // card saw and when each call started and ended (arp-java-.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. import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import type { IPv4DeliveryResult, NetworkRuntimeEngine } from '../src/index.js'; import { GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame } from './javaRun.js'; import type { Scenario } from './scenario.js'; const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios'); const HOST = '00000000-0000-0000-0000-000000000301'; interface JavaRun { frames: Record; events: [number, string][]; } /** A ping's result in Java's words: the ICMP answer's type and code, or null when none came. */ function pingResult(result: IPv4DeliveryResult, answered: boolean): string { if (result.type === 'icmpError' && result.kind === 'destinationHostUnreachable') return 'ICMP 3/1'; return answered ? 'ICMP 0/0' : 'null'; } const STORIES: Record 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); }, 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 = { 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", }; describe('ARP as Java FILIUS runs it', () => { const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'arp-routed.scenario.json'), 'utf8')) as Scenario; for (const [name, story] of Object.entries(STORIES)) { it(name, () => { 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; 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 [[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); } }); } });