Files
mdg-filius/packages/core/test/dnsJava.test.ts
T
julian.kiedaischandClaude Opus 5.5 dbbc1fd70d feat(core): ARP after Java, measured with Java FILIUS
An ARP lookup asks, waits a round trip, asks once more and gives up after
another round trip; then host unreachable, observed at the sender itself or
sent back by the router (for ICMP only for an echo request). A waiting
sender wakes after what else happens at that time. A ping then waits a
round trip for its answer, counted from when it left. Swift asks twice at
once while events run and lets the packet wait without end when the
address exists anywhere in the network; the option swiftArp keeps that for
the Swift comparison, and three scenarios are compared with Swift until the
trace where ARP fails or a ping goes unanswered.

arpJava.test.ts compares ArpRun: both pings match; the two UDP stories wait
for Java's transport thread (next step). DNS without a server now matches
Java too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:11:23 +00:00

88 lines
4.3 KiB
TypeScript

// DNS as Java FILIUS runs it. tools/java-oracle's DnsRun called a client's
// resolver on dns-lan's network in Java FILIUS and wrote every frame each
// card saw and what the call returned (dns-java-<story>.java.json). The
// engine makes the same call: the frames must come in the same order, with
// the same content, at about the same distance from each other (javaRun.ts),
// the call must return the same, after about as long.
//
// Java draws each message's ID with Math.random and a client's free port with
// its random; both are compared as placeholders, and an answer must carry the
// ID of its question.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import type { DnsAddressResolution, NetworkRuntimeEngine } from '../src/index.js';
import { GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame, type SeenFrame } from './javaRun.js';
import type { Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const CLIENT = '00000000-0000-0000-0000-000000000101';
const LOST_CLIENT = '00000000-0000-0000-0000-000000000104';
interface JavaRun {
frames: Record<string, JavaFrame[]>;
events: [number, string][];
}
const STORIES: Record<string, (engine: NetworkRuntimeEngine) => DnsAddressResolution> = {
address: (engine) => engine.resolveDnsAddress(CLIENT, 'web.filius.de'),
mail: (engine) => engine.resolveDnsMailServer(CLIENT, 'filius.de'),
referral: (engine) => engine.resolveDnsAddress(CLIENT, 'www.schule.de'),
unknown: (engine) => engine.resolveDnsAddress(CLIENT, 'x.unbekannt.org'),
noServer: (engine) => engine.resolveDnsAddress(LOST_CLIENT, 'web.filius.de'),
};
/** Java's words for the result: the address, null, or the exception. */
function javaResult(resolution: DnsAddressResolution): string {
return resolution.type === 'timedOut' ? 'failed TimeOutException' : `returned ${resolution.address ?? 'null'}`;
}
/** IDs and free ports as placeholders; each ID stands for the question it first appeared in. */
function normalized(frames: readonly SeenFrame[]): SeenFrame[] {
const ids = new Map<string, string>();
return frames.map((frame) => ({
time: frame.time,
text: frame.text
.replace(/ID=(\d+)/, (_, id: string) => {
if (!ids.has(id)) ids.set(id, `q${ids.size + 1}`);
return `ID=${ids.get(id)}`;
})
.replace(/:(\d{5})\b/g, (whole, port: string) => (Number(port) >= 49_152 ? ':client' : whole)),
}));
}
/** Stories the engine does not play like Java yet, with the reason; each must still differ from Java. */
const NOT_YET_LIKE_JAVA: Record<string, string> = {};
describe('DNS as Java FILIUS runs it', () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'dns-lan.scenario.json'), 'utf8')) as Scenario;
for (const [name, story] of Object.entries(STORIES)) {
it(name, () => {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `dns-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 [[javaCalled], [javaReturned, javaOutcome]] = java.events as [[number, string], [number, string]];
expect(`${javaOutcome.split(' ').slice(-2).join(' ')}`).toBe(javaResult(result).split(' ').slice(-2).join(' '));
const expected = gaps(normalized(javaFrames(java.frames)));
const actual = gaps(normalized(frames()));
if (name in NOT_YET_LIKE_JAVA) {
expect(JSON.stringify(actual), NOT_YET_LIKE_JAVA[name]).not.toBe(JSON.stringify(expected));
return;
}
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);
}
});
}
});