// 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); }); });