feat: F4.7 the data exchange window, measured against Java's
One row per frame (its highest layer's), Java's detail tree, the table as Java's text file, "Tabellen löschen" for every card (clearCapture in the room), a tab per card with Java's title, Java's layer colours as tokens, the chosen frame and autoscroll per tab. New oracle ExchangeWindowRun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// The data exchange window's side of the room, on FILIUS' example
|
||||
// nat_with_mapped_port: a card's frames as the window shows them (one row
|
||||
// per frame from its rows, the details from its layered rows), "Tabellen
|
||||
// löschen" — every card's list and the dropped frames emptied for every
|
||||
// client, numbered anew, the cables still lighting up —, refused while the
|
||||
// design is shown, and the lists kept over a stop and a start, as Java's
|
||||
// Lauscher keeps them. The window's rows against Java's are
|
||||
// packages/core/test/exchangeWindow.test.ts.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
import { readProject, setSoftware, writeProject } from '@mdg-filius/doc';
|
||||
import { exchangeRow } from '@mdg-filius/core';
|
||||
import { openProjectArchive } from '@mdg-filius/fls';
|
||||
import type { HostMessage, TraceFrame } from '@mdg-filius/protocol';
|
||||
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
|
||||
|
||||
const corpus = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus/java-examples');
|
||||
|
||||
function setUp() {
|
||||
let counter = 0;
|
||||
const doc = new Y.Doc();
|
||||
writeProject(
|
||||
doc,
|
||||
openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'nat_with_mapped_port.fls'))), () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`),
|
||||
);
|
||||
const project = readProject(doc);
|
||||
const webServer = project.graph.nodes.find((node) => node.displayName === 'Webserver - 192.168.0.10')!;
|
||||
const port = webServer.ports[0]!.id;
|
||||
const link = project.graph.links.find((each) => each.sourcePortId === port || each.targetPortId === port)!.id;
|
||||
setSoftware(doc, webServer.id, { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||
let now = 0;
|
||||
const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 });
|
||||
const connect = () => {
|
||||
const traces: TraceFrame[] = [];
|
||||
const resets: number[] = [];
|
||||
const dropped: number[] = [];
|
||||
const lit: string[][] = [];
|
||||
const errors: string[] = [];
|
||||
const id = room.connect((message: HostMessage) => {
|
||||
if (message.kind === 'trace' && message.portId === port) {
|
||||
if (message.reset) {
|
||||
resets.push(message.frames.length);
|
||||
traces.length = 0;
|
||||
}
|
||||
traces.push(...message.frames);
|
||||
}
|
||||
if (message.kind === 'dropped' && message.reset) dropped.push(message.units.length);
|
||||
if (message.kind === 'activity') lit.push([...message.links]);
|
||||
if (message.kind === 'error') errors.push(message.code);
|
||||
});
|
||||
return { id, traces, resets, dropped, lit, errors };
|
||||
};
|
||||
const send = (who: number, fields: object) => room.receive(who, { v: 1, ...fields });
|
||||
const run = (milliseconds: number) => {
|
||||
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
|
||||
now += TICK_MILLISECONDS;
|
||||
room.tick();
|
||||
}
|
||||
};
|
||||
const ping = (who: number, target: string) => send(who, { kind: 'input', node: webServer.id, app: 'terminal', action: 'enter', args: { line: `ping ${target}` }, reqId: target });
|
||||
return { port, link, connect, send, run, ping };
|
||||
}
|
||||
|
||||
describe('the data exchange window in the room', () => {
|
||||
it('shows a card\'s frames, and "Tabellen löschen" empties every list for every client and numbers anew', () => {
|
||||
const { port, link, connect, send, run, ping } = setUp();
|
||||
const first = connect();
|
||||
const second = connect();
|
||||
for (const client of [first, second]) send(client.id, { kind: 'sub', topics: [`trace:${port}`, 'dropped', 'activity'] });
|
||||
// Not while the design is shown: Java's window is not there.
|
||||
send(first.id, { kind: 'clearCapture' });
|
||||
expect(first.errors).toEqual(['notRunning']);
|
||||
|
||||
send(first.id, { kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
ping(first.id, '192.168.0.1');
|
||||
run(5_000);
|
||||
// An ARP exchange and four pings with their answers: one row each, the highest layer's; the details from all of them.
|
||||
expect(first.traces.map((frame) => exchangeRow(frame.rows).protocol)).toEqual(['ARP', 'ARP', ...Array.from({ length: 8 }, () => 'ICMP')]);
|
||||
expect(first.traces.map((frame) => frame.number)).toEqual(Array.from({ length: 10 }, (_, index) => index + 1));
|
||||
expect(new Set(first.traces.map((frame) => frame.layeredRows.map((row) => row.layer).join()))).toEqual(new Set(['networkAccess,internet']));
|
||||
expect(second.traces).toEqual(first.traces);
|
||||
// Something dropped, to be emptied with the rest.
|
||||
send(first.id, { kind: 'drop', on: true });
|
||||
ping(first.id, '192.168.0.1');
|
||||
run(5_000);
|
||||
send(first.id, { kind: 'drop', on: false });
|
||||
const droppedBefore = second.dropped.length;
|
||||
|
||||
// Either client empties the lists of both.
|
||||
send(second.id, { kind: 'clearCapture' });
|
||||
for (const client of [first, second]) {
|
||||
expect(client.resets.at(-1)).toBe(0);
|
||||
expect(client.traces).toEqual([]);
|
||||
expect(client.dropped.at(-1)).toBe(0);
|
||||
}
|
||||
expect(second.dropped.length).toBe(droppedBefore + 1);
|
||||
// Numbered from 1 again, and the cable lights up with the first of them.
|
||||
ping(first.id, '192.168.0.1');
|
||||
for (let waited = 0; first.traces.length === 0 && waited < 5_000; waited += TICK_MILLISECONDS) run(TICK_MILLISECONDS);
|
||||
expect(first.traces.map((frame) => frame.number)).toEqual([1]);
|
||||
expect(first.lit.at(-1)).toContain(link);
|
||||
run(5_000);
|
||||
expect(first.traces.map((frame) => frame.number)).toEqual(first.traces.map((_, index) => index + 1));
|
||||
expect(first.traces.length).toBeGreaterThanOrEqual(8);
|
||||
expect(second.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the lists over a stop and a start, numbering on', () => {
|
||||
const { port, connect, send, run, ping } = setUp();
|
||||
const client = connect();
|
||||
send(client.id, { kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
ping(client.id, '192.168.0.1');
|
||||
run(5_000);
|
||||
send(client.id, { kind: 'stop' });
|
||||
send(client.id, { kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
ping(client.id, '192.168.0.1');
|
||||
run(5_000);
|
||||
// Watched after the second start: the first run's frames are there, the second's go on from them.
|
||||
send(client.id, { kind: 'sub', topics: [`trace:${port}`] });
|
||||
expect(client.resets).toEqual([client.traces.length]);
|
||||
expect(client.traces.map((frame) => frame.number)).toEqual(client.traces.map((_, index) => index + 1));
|
||||
// Each run: an ARP exchange (a start forgets what ARP learned) and four pings with their answers.
|
||||
expect(client.traces.length).toBe(20);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user