feat(core): the packet capture's table after Java's Lauscher, measured against Java's own tables
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>
This commit is contained in:
@@ -22,7 +22,15 @@
|
||||
// (engine.test.ts checks that: one cable's delay is below this tolerance).
|
||||
|
||||
import { expect } from 'vitest';
|
||||
import { NetworkRuntimeEngine, TCP_FLAG, type EthernetFrame } from '../src/index.js';
|
||||
import {
|
||||
captureCommentText,
|
||||
captureRows,
|
||||
NetworkRuntimeEngine,
|
||||
TCP_FLAG,
|
||||
type CapturedFrame,
|
||||
type CaptureLayer,
|
||||
type EthernetFrame,
|
||||
} from '../src/index.js';
|
||||
import { snapshotOf, type Scenario } from './scenario.js';
|
||||
|
||||
export const GAP_TOLERANCE_MILLISECONDS = 120;
|
||||
@@ -177,15 +185,21 @@ export function expectSameCaptures(
|
||||
until?: number | undefined;
|
||||
tolerance?: number;
|
||||
starts?: { java: number; engine: number };
|
||||
/** Java's tables of the cards (ScenarioNetwork.writeRows), compared for the frames compared. */
|
||||
tables?: JavaTables;
|
||||
/** Placeholders in a table cell, beyond those of tableCell. */
|
||||
cell?: (text: string) => string;
|
||||
} = {},
|
||||
): void {
|
||||
const text = options.text ?? ((each: string) => each);
|
||||
const engineByCard = new Map<string, SeenFrame[]>();
|
||||
const capturedByCard = new Map<string, readonly CapturedFrame[]>();
|
||||
for (const node of scenario.nodes) {
|
||||
if (node.kind === 'networkSwitch') continue;
|
||||
node.ports.forEach((port, index) => {
|
||||
const frames = engine.capture.frames(port.id).map((each) => ({ time: each.timeMilliseconds, text: text(engineText(each.frame)) }));
|
||||
engineByCard.set(index === 0 ? node.id : `${node.id}#${index}`, frames);
|
||||
const card = index === 0 ? node.id : `${node.id}#${index}`;
|
||||
capturedByCard.set(card, engine.capture.frames(port.id));
|
||||
engineByCard.set(card, capturedByCard.get(card)!.map((each) => ({ time: each.timeMilliseconds, text: text(engineText(each.frame)) })));
|
||||
});
|
||||
}
|
||||
const javaByCard = new Map(Object.entries(java).map(([card, frames]) => [card, frames.map((frame) => ({ time: frame.time, text: text(javaText(frame)) }))]));
|
||||
@@ -198,7 +212,29 @@ export function expectSameCaptures(
|
||||
const normalized = options.normalized ?? ((frames: SeenFrame[]) => frames);
|
||||
const window = (frames: SeenFrame[], start: number) =>
|
||||
frames.filter((frame) => frame.time - start >= from && frame.time - start < until).map((frame) => ({ time: frame.time - start, text: frame.text }));
|
||||
const inWindow = (time: number, start: number) => time - start >= from && time - start < until;
|
||||
for (const [card, javaList] of javaByCard) {
|
||||
if (options.tables !== undefined) {
|
||||
// The rows of the frames in the window, frame by frame.
|
||||
const javaNumbers = javaList.flatMap((frame, index) => (inWindow(frame.time, javaStart) ? [index + 1] : []));
|
||||
const engineFrames = capturedByCard.get(card)!.filter((each) => inWindow(each.timeMilliseconds, engineStart));
|
||||
for (const [view, withAddresses] of [['rows', true], ['layeredRows', false]] as const) {
|
||||
const javaRows = options.tables[view]?.[card];
|
||||
if (javaRows === undefined) continue;
|
||||
const engineRows = captureRows(engineFrames, { withAddresses });
|
||||
const cell = (value: string | null) => (value === null ? null : (options.cell ?? ((each: string) => each))(tableCell(value)));
|
||||
for (const [index, number] of javaNumbers.entries()) {
|
||||
const frame = engineFrames[index];
|
||||
const expectedRows = javaRows.filter((row) => row[0] === String(number)).map((row) => row.slice(1).map(cell));
|
||||
// A frame a test added where Java errs (JAVA_BUGS) has no table of Java's.
|
||||
if (expectedRows.length === 0 && number > Math.max(0, ...javaRows.map((row) => Number(row[0])))) continue;
|
||||
const actualRows = engineRows
|
||||
.filter((row) => row.number === frame?.number)
|
||||
.map((row) => [row.source, row.destination, row.protocol, JAVA_LAYERS[row.layer], captureCommentText(row.comment, JAVA_WORDS, javaNumber)].map(cell));
|
||||
expect(actualRows, `${card} ${view} of frame #${index + 1}`).toEqual(expectedRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
const expected = normalized(window(javaList, javaStart));
|
||||
const actual = normalized(window(engineByCard.get(card)!, engineStart));
|
||||
for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) {
|
||||
@@ -214,3 +250,36 @@ export function expectSameCaptures(
|
||||
export function withClientPortText(text: string): string {
|
||||
return text.replace(/:(\d{5})\b/g, (whole, port: string) => (Number(port) >= 49_152 ? ':client' : whole));
|
||||
}
|
||||
|
||||
/** Java's capture tables per card: number, source, destination, protocol, layer, comment (ScenarioNetwork.writeRows). */
|
||||
export interface JavaTables {
|
||||
rows?: Record<string, (string | null)[][]>;
|
||||
layeredRows?: Record<string, (string | null)[][]>;
|
||||
}
|
||||
|
||||
/** Java's layer names and comment words: its German message bundle, the locale it runs in (Locale.GERMANY). */
|
||||
const JAVA_LAYERS: Record<CaptureLayer, string> = { networkAccess: 'Netzzugang', internet: 'Vermittlung', transport: 'Transport', application: 'Anwendung' };
|
||||
const JAVA_WORDS = { protocol: 'Protokoll', searchForMac: 'Suche nach MAC f\u00fcr', macIs: 'MAC ist' };
|
||||
|
||||
/** NumberFormat for Locale.GERMANY: groups of three digits with dots. */
|
||||
function javaNumber(value: number): string {
|
||||
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholders every test needs in a table cell: free ports, DNS message IDs,
|
||||
* IP packet IDs, link-local addresses. And two departures from Java: its ARP
|
||||
* reply to 0.0.0.0 writes the broadcast address in lower case
|
||||
* (ARP.sendArpReply), everything else in upper case — the engine always in
|
||||
* upper case (better than Java where nothing depends on it); and Java
|
||||
* misspells "ICMP Time Exeeded" (a typo, not taken).
|
||||
*/
|
||||
function tableCell(text: string): string {
|
||||
return withClientPortText(text)
|
||||
.replaceAll('ff:ff:ff:ff:ff:ff', 'FF:FF:FF:FF:FF:FF')
|
||||
.replace('ICMP Time Exeeded (poof)', 'ICMP Time Exceeded (poof)')
|
||||
.replace(/^(\d{5})$/, (whole, port: string) => (Number(port) >= 49_152 ? 'client' : whole))
|
||||
.replace(/ID=\d+/g, 'ID=x')
|
||||
.replace(/\[id=\d+/g, '[id=x')
|
||||
.replace(/169\.254\.1\.\d+/g, '169.254.1.x');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user