feat(core): packet capture per card after Java's Lauscher, measured card by card
The engine keeps Java's packet capture (capture.ts): per network card every frame it sent (when it leaves on the cable) or received (also one for another card), once, with time and number; switches log nothing; frames the packet loss dropped are kept apart; the capture outlives a stop and a new start, as in Java, until clearCapture. Per port instead of per MAC, at most 10,000 frames a card. expectSameCaptures compares each card's capture with Java's list of that card in the ARP, WLAN, DNS, TCP, NAT, RIP and single-client DHCP runs. It found that ScenarioNetwork ignored a switch's learning time (Java ran 300 s, the core the scenarios' 5 s); the oracle sets it now, and the TCP and DHCP runs are recorded again. A core test found that a new start numbers frames from 1 again and the capture took them as seen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
// time (250 ms) may not. A cable carries one frame after the other in both
|
||||
// (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 { snapshotOf, type Scenario } from './scenario.js';
|
||||
|
||||
@@ -154,3 +155,62 @@ export function javaFrames(framesByNode: Record<string, JavaFrame[]>): SeenFrame
|
||||
export function gaps(frames: readonly SeenFrame[]) {
|
||||
return frames.map((frame, index) => ({ text: frame.text, gap: index === 0 ? 0 : frame.time - frames[index - 1]!.time }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Each card's packet capture against Java's (the Lauscher's list of the
|
||||
* card, as the oracle wrote it: under the node's id for its first card,
|
||||
* `id#index` for the others): the same frames in the same order, each at
|
||||
* about the same time (both counted from the run's first frame, or from
|
||||
* `starts`). `text`
|
||||
* puts placeholders in a frame's text (ports, IDs a test cannot predict),
|
||||
* `normalized` does so for a card's whole list; with `until` (Java's time),
|
||||
* only the frames before it; with `from`, only those from it.
|
||||
*/
|
||||
export function expectSameCaptures(
|
||||
engine: NetworkRuntimeEngine,
|
||||
scenario: Scenario,
|
||||
java: Record<string, JavaFrame[]>,
|
||||
options: {
|
||||
text?: (text: string) => string;
|
||||
normalized?: (frames: SeenFrame[]) => SeenFrame[];
|
||||
from?: number | undefined;
|
||||
until?: number | undefined;
|
||||
tolerance?: number;
|
||||
starts?: { java: number; engine: number };
|
||||
} = {},
|
||||
): void {
|
||||
const text = options.text ?? ((each: string) => each);
|
||||
const engineByCard = new Map<string, SeenFrame[]>();
|
||||
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 javaByCard = new Map(Object.entries(java).map(([card, frames]) => [card, frames.map((frame) => ({ time: frame.time, text: text(javaText(frame)) }))]));
|
||||
expect([...engineByCard.keys()].sort()).toEqual([...javaByCard.keys()].sort());
|
||||
const first = (byCard: Map<string, SeenFrame[]>) => Math.min(...[...byCard.values()].flat().map((frame) => frame.time));
|
||||
const engineStart = options.starts?.engine ?? first(engineByCard);
|
||||
const javaStart = options.starts?.java ?? first(javaByCard);
|
||||
const from = (options.from ?? Number.NEGATIVE_INFINITY) - javaStart;
|
||||
const until = (options.until ?? Number.POSITIVE_INFINITY) - javaStart;
|
||||
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 }));
|
||||
for (const [card, javaList] of javaByCard) {
|
||||
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) {
|
||||
expect(actual[index]?.text, `${card} frame #${index + 1}`).toBe(expected[index]?.text);
|
||||
expect(Math.abs(actual[index]!.time - expected[index]!.time), `${card} time of frame #${index + 1}`).toBeLessThanOrEqual(
|
||||
options.tolerance ?? GAP_TOLERANCE_MILLISECONDS,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A client's free port as "client": Java draws it at random, the engine from its seed. */
|
||||
export function withClientPortText(text: string): string {
|
||||
return text.replace(/:(\d{5})\b/g, (whole, port: string) => (Number(port) >= 49_152 ? ':client' : whole));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user