feat: F4.6g5 the switch and NAT tables and "Pakete verwerfen", measured against Java's
ViewersRun takes the rows of Java's viewers from the firmware: a switch's table, a home router's NAT table, each after its reset, and the frames "Pakete verwerfen" drops while it is held. The core writes a frame as Java does and resets the NAT's dynamic entries; the room sends both tables as views, drops while any client holds the button, and sends the dropped frames, begun anew with each start, with the clock their times of day are written from; the editor opens a table with a tap on a switch or home router and holds "Pakete verwerfen" in its toolbar, which now wraps on a phone. This completes F4.6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// What FILIUS' viewers of a running simulation show, against Java's
|
||||
// (viewers-switch.java.json, viewers-nat.java.json; ViewersRun): a switch's
|
||||
// source address table after pings, after its reset and after the retention;
|
||||
// a home router's NAT table after a ping and a datagram, and after its reset
|
||||
// (the port forwarding's entry stays); the frames "Pakete verwerfen" drops
|
||||
// while it is held, in Java's words. Java's time of day is the viewer's to
|
||||
// write (the editor's), and its table of a switch comes in the order of
|
||||
// Java's hash map, here by address.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { javaFrameText, natViewRow, NetworkRuntimeEngine, type Steps } from '../src/index.js';
|
||||
import { snapshotOf, type Scenario } from './scenario.js';
|
||||
|
||||
interface JavaStep {
|
||||
readonly step: string;
|
||||
readonly at: number;
|
||||
readonly rows?: readonly (readonly string[])[];
|
||||
readonly updated?: Readonly<Record<string, number>>;
|
||||
readonly answer?: string;
|
||||
readonly answers?: Readonly<Record<string, string | number>>;
|
||||
readonly dropped?: readonly string[];
|
||||
}
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const javaRun = (name: string) => JSON.parse(readFileSync(join(here, `viewers-${name}.java.json`), 'utf8')) as { readonly steps: readonly JavaStep[] };
|
||||
const scenarioOf = (name: string) => JSON.parse(readFileSync(join(here, `scenarios/${name}.scenario.json`), 'utf8')) as Scenario;
|
||||
|
||||
function started(scenario: Scenario) {
|
||||
const engine = new NetworkRuntimeEngine(scenario.seed);
|
||||
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
|
||||
const run = (milliseconds: number) => engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + milliseconds });
|
||||
/** A ping, returning when its answer came or its wait ended, as Java's sendProbe blocks. */
|
||||
const ping = (from: string, target: string, sequenceNumber: number): string => {
|
||||
let answer: string | undefined;
|
||||
engine.startThread(
|
||||
(function* (): Steps {
|
||||
const reply = yield* engine.icmpEchoSteps(from, target, { identifier: 1, sequenceNumber });
|
||||
answer = reply === undefined ? 'null' : `ICMP ${reply.message.kind === 'echoReply' ? '0/0' : reply.message.kind}`;
|
||||
})(),
|
||||
);
|
||||
for (let waited = 0; answer === undefined && waited < 30_000; waited += 10) run(10);
|
||||
return answer ?? 'null';
|
||||
};
|
||||
return { engine, run, ping };
|
||||
}
|
||||
|
||||
describe("a switch's table and the dropped frames, against Java's", () => {
|
||||
const java = javaRun('switch');
|
||||
const at = (name: string) => java.steps.find((step) => step.step === name)!;
|
||||
const SWITCH = '00000000-0000-0000-0000-000000000104';
|
||||
const PC10 = '00000000-0000-0000-0000-000000000101';
|
||||
|
||||
it('learns, is reset, ages, and lists what is dropped while it is held', () => {
|
||||
const { engine, run, ping } = started(scenarioOf('switch-lan'));
|
||||
run(500);
|
||||
/** The table as Java's viewer shows it, without the time: address in capitals, "Port <n>". */
|
||||
const table = () => engine.switchSatEntries(SWITCH).map((entry) => [entry.macAddress.toUpperCase(), `Port ${entry.portNumber}`]);
|
||||
const javaTable = (name: string) => at(name).rows!.map(([mac = '', port = '']) => [mac, port]);
|
||||
const sorted = (rows: string[][]) => [...rows].sort(([lhs], [rhs]) => (lhs! < rhs! ? -1 : 1));
|
||||
expect([ping(PC10, '192.168.0.20', 1), ping(PC10, '192.168.0.30', 2)]).toEqual(Object.values(at('after pings').answers!));
|
||||
// By address (Java's in its hash map's order).
|
||||
expect(table()).toEqual(sorted(javaTable('after pings')));
|
||||
expect(table()).not.toEqual(javaTable('after pings'));
|
||||
engine.clearSwitchSat(SWITCH);
|
||||
expect(table()).toEqual(javaTable('reset'));
|
||||
expect(ping(PC10, '192.168.0.20', 3)).toBe(at('ping .20 again').answers!['ping .20']);
|
||||
expect(table()).toEqual(sorted(javaTable('ping .20 again')));
|
||||
run(6000);
|
||||
expect(table()).toEqual(javaTable('after the retention'));
|
||||
|
||||
// "Pakete verwerfen" held while .10 pings .30: the request is dropped on its first cable; released, the ping is answered.
|
||||
engine.setGlobalPacketLossEnabled(true);
|
||||
const dropped = ping(PC10, '192.168.0.30', 4);
|
||||
engine.setGlobalPacketLossEnabled(false);
|
||||
const text = (step: JavaStep) => step.dropped!.map((line) => line.slice(line.indexOf(' : ') + 3));
|
||||
const ours = () => engine.capture.droppedFrames().map((each) => javaFrameText(each.frame));
|
||||
const sameButIdentity = (lines: readonly string[]) => lines.map((line) => line.replace(/\[id=\d+;/, '[id=ID;'));
|
||||
expect(dropped).toBe(at('dropped while held').answer);
|
||||
expect(sameButIdentity(ours())).toEqual(sameButIdentity(text(at('dropped while held'))));
|
||||
expect(ping(PC10, '192.168.0.30', 5)).toBe(at('released').answer);
|
||||
expect(sameButIdentity(ours())).toEqual(sameButIdentity(text(at('released'))));
|
||||
// Java's time of the drop is the time of day with milliseconds.
|
||||
expect(at('released').dropped![0]).toMatch(/^\d\d:\d\d:\d\d\.\d\d\d : \[src=/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a home router's NAT table, against Java's", () => {
|
||||
const java = javaRun('nat');
|
||||
const at = (name: string) => java.steps.find((step) => step.step === name)!;
|
||||
const GATEWAY = '00000000-0000-0000-0000-000000000502';
|
||||
const LAN_PC = '00000000-0000-0000-0000-000000000501';
|
||||
|
||||
it('shows the port forwarding, a ping and a datagram, and keeps the port forwarding when reset', () => {
|
||||
const { engine, run, ping } = started(scenarioOf('nat-home'));
|
||||
run(500);
|
||||
const rows = () =>
|
||||
engine.natEntries(GATEWAY).map((entry) => {
|
||||
const row = natViewRow(entry);
|
||||
return [...row.cells, row.updatedAtMilliseconds === undefined ? '-' : 'TIME'];
|
||||
});
|
||||
/** Java's rows with a time of day as TIME, and the datagram's ports (drawn) as PORT. */
|
||||
const javaRows = (name: string) =>
|
||||
at(name).rows!.map((row) => (row[0] === 'UDP' ? [row[0], 'PORT', row[2], row[3], 'PORT', 'TIME'] : row.map((cell, index) => (index === 5 && cell !== '-' ? 'TIME' : cell))));
|
||||
expect(rows()).toEqual(javaRows('start'));
|
||||
expect(ping(LAN_PC, '42.0.0.20', 1)).toBe(at('after ping and datagram').answers!['ping 42.0.0.20']);
|
||||
const socket = engine.bindUdpSocket(LAN_PC, { remoteIPAddress: '42.0.0.20', remotePort: 7 })!;
|
||||
engine.sendUdp(socket, new TextEncoder().encode('one'));
|
||||
run(1500);
|
||||
expect(rows().map((row) => (row[0] === 'UDP' ? [row[0], 'PORT', row[2], row[3], 'PORT', row[5]] : row))).toEqual(javaRows('after ping and datagram'));
|
||||
engine.clearDynamicNatEntries(GATEWAY);
|
||||
expect(rows()).toEqual(javaRows('reset'));
|
||||
});
|
||||
|
||||
it('writes an entry as Java’s createEintrag does', () => {
|
||||
expect([
|
||||
natViewRow({ protocol: 'tcp', remoteIPAddress: '', port: 8080, lan: { ipAddress: '192.168.0.10', port: 80 }, type: 'static', updatedAtMilliseconds: 5 }),
|
||||
natViewRow({ protocol: 'icmp', remoteIPAddress: '42.0.0.20', port: 3, lan: { ipAddress: '192.168.0.10', port: 4 }, type: 'dynamic', updatedAtMilliseconds: 7 }),
|
||||
natViewRow({ protocol: 'udp', remoteIPAddress: '42.0.0.30', port: 8080, lan: { ipAddress: '192.168.0.10', port: 80 }, type: 'dynamicFromStatic', updatedAtMilliseconds: 9 }),
|
||||
]).toEqual([
|
||||
{ cells: ['TCP', '8080', '*', '192.168.0.10', '80'] },
|
||||
{ cells: ['ICMP', '0 (Identifier: 3)', '42.0.0.20', '192.168.0.10', '0 (Identifier: 4)'], updatedAtMilliseconds: 7 },
|
||||
{ cells: ['UDP', '8080', '42.0.0.30', '192.168.0.10', '80'], updatedAtMilliseconds: 9 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user