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>
133 lines
6.4 KiB
TypeScript
133 lines
6.4 KiB
TypeScript
// 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);
|
|
});
|
|
});
|