captureRows.ts builds Java's rows per frame (Lauscher.datenVorbereiten): Ethernet; ARP, ICMP or IP; TCP or UDP; the data with Java's protocol recognition; with addresses and ports or ports only. The comment stays structured; captureCommentText says it with given words and numbers. ScenarioNetwork writes Java's finished tables per card (Lauscher.getDaten, German locale); all runtime runs are recorded again, NatRun has ttlOut. expectSameCaptures compares both views for every frame. Fixed where the engine differed: an ARP request's target MAC is the broadcast address, an ICMP error's identifier and sequence number are 0 (RFC 792), and host unreachable from a router carries the packet as forwarded. Not taken: Java's lower-case broadcast address in a reply to 0.0.0.0, and the typo 'Exeeded'. RIP keeps its recordings: Java's staggered router start now gives a different startup on this machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
4.5 KiB
TypeScript
83 lines
4.5 KiB
TypeScript
// WLAN as Java FILIUS runs it. tools/java-oracle's WlanRun pinged on
|
|
// wlan-lan's network in Java FILIUS — notebooks with wireless cards joined to
|
|
// the switch's SSID by wireless cables, a PC on a plain cable, a notebook on
|
|
// the same SSID without a cable — and wrote every frame each card saw and
|
|
// when each call started and ended (wlan-java-<story>.java.json). In Java a
|
|
// wireless cable is a cable like any other at run time, and an SSID connects
|
|
// nothing: only the editor reads it, to lay or remove the wireless cable
|
|
// (JHostKonfiguration.updateWifiConnection). The engine runs the scenario's
|
|
// links and knows no SSID; it 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 end at about the same times and
|
|
// return the same. Swift instead joins a host to the switch with its SSID at
|
|
// run time, also without a cable (wirelessAssociations); not taken.
|
|
|
|
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 { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame, type JavaTables } from './javaRun.js';
|
|
import type { Scenario } from './scenario.js';
|
|
|
|
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
|
|
const NOTEBOOK = '00000000-0000-0000-0000-000000000601';
|
|
const UNCABLED = '00000000-0000-0000-0000-000000000605';
|
|
|
|
interface JavaRun extends JavaTables {
|
|
frames: Record<string, JavaFrame[]>;
|
|
events: [number, string][];
|
|
}
|
|
|
|
/** A ping in Java's words: the ICMP answer's type and code, or null when none came in time. */
|
|
function ping(engine: NetworkRuntimeEngine, from: Id, target: string): string {
|
|
const result = engine.sendIcmpEcho(from, target);
|
|
if (result.type === 'icmpError' && result.kind === 'destinationHostUnreachable') return 'ICMP 3/1';
|
|
const answered = (engine.state.icmpObservationsByNodeId.get(from) ?? []).some((observation) => observation.message.kind === 'echoReply');
|
|
return answered ? 'ICMP 0/0' : 'null';
|
|
}
|
|
|
|
const STORIES: Record<string, [Id, string]> = {
|
|
pingWired: [NOTEBOOK, '192.168.1.13'],
|
|
pingWireless: [NOTEBOOK, '192.168.1.12'],
|
|
pingWithoutCable: [NOTEBOOK, '192.168.1.14'],
|
|
pingFromWithoutCable: [UNCABLED, '192.168.1.11'],
|
|
};
|
|
|
|
describe('WLAN as Java FILIUS runs it', () => {
|
|
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'wlan-lan.scenario.json'), 'utf8')) as Scenario;
|
|
|
|
it('has wireless cables and an SSID without a cable to compare', () => {
|
|
expect(scenario.links.filter((link) => link.wireless === true)).toHaveLength(2);
|
|
const cabled = new Set(scenario.links.flatMap((link) => [link.sourceNode, link.targetNode]));
|
|
expect(cabled.has(UNCABLED)).toBe(false);
|
|
expect(scenario.wireless?.[UNCABLED]?.ssid).toBe(Object.values(scenario.switches ?? {})[0]?.ssid);
|
|
});
|
|
|
|
for (const [name, [from, target]] of Object.entries(STORIES)) {
|
|
it(name, () => {
|
|
const java = JSON.parse(readFileSync(join(scenarioDirectory, `wlan-java-${name}.java.json`), 'utf8')) as JavaRun;
|
|
const { engine, frames } = watchedEngine(scenario);
|
|
engine.handle({ type: 'advance', toMilliseconds: 500 });
|
|
const events: [number, string][] = [];
|
|
const label = `sendProbe ${target}`;
|
|
events.push([engine.state.currentTimeMilliseconds, `${label} called`]);
|
|
const result = ping(engine, from, target);
|
|
events.push([engine.state.currentTimeMilliseconds, `${label} returned ${result}`]);
|
|
engine.handle({ type: 'advance', toMilliseconds: 12_000 });
|
|
|
|
expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => event.replace(/ from .*$/, '')));
|
|
expect(Math.abs(events[1]![0] - events[0]![0] - (java.events[1]![0] - java.events[0]![0])), 'duration of the ping').toBeLessThanOrEqual(
|
|
GAP_TOLERANCE_MILLISECONDS,
|
|
);
|
|
const expected = gaps(javaFrames(java.frames));
|
|
const actual = gaps(frames());
|
|
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);
|
|
}
|
|
expectSameCaptures(engine, scenario, java.frames, { tables: java });
|
|
});
|
|
}
|
|
});
|