// 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-.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 } 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 { frames: Record; 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 = { 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); }); } });