Oracle EchoWindowRun uses Java's echo server and client windows as a person does; SaveAfterRunRun echo saves what Java keeps after the run. The room takes setActive with a port, connect/disconnect/send, sends both views and writes the server's port and the client's target back. The editor's windows show Java's buttons, fields and lines; a port that is no port is marked and not handed over (Java's button throws). Core fix: an empty message (one empty segment with the push, as Java's erstelleSegmente makes) counted as a repeat and never arrived. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
252 lines
14 KiB
TypeScript
252 lines
14 KiB
TypeScript
// The echo server's and the generic client's windows in the room, against
|
|
// Java's (echoWindow.java.json, EchoWindowRun): Java's steps played as the
|
|
// editor's panels play them — the start button with the port typed,
|
|
// "Verbinden" with the address and port typed (or "Trennen" while
|
|
// connected), "Senden" only while connected —, the buttons, fields and lines
|
|
// both windows show after each; and what Java's project keeps of them after
|
|
// a run (fixtures/after-run-echo.fls, SaveAfterRunRun).
|
|
//
|
|
// Named: a port that is no number is not handed to the room (Java's window
|
|
// throws and does nothing; the panel marks the field); a connection attempt
|
|
// lasts three SYNs (F2.3c), so the one to the stopped server is awaited
|
|
// before it starts again. The client's own port is the threads' draw, in
|
|
// Java as here: written as PORT.
|
|
|
|
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 type { EchoServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
|
|
import { addDevice, connect, emptyProject, readProject, setHostAddress, setSoftware, writeProject } from '@mdg-filius/doc';
|
|
import { openProjectArchive } from '@mdg-filius/fls';
|
|
import { portOfText, type EchoServerView, type HostMessage, type SimpleClientView } from '@mdg-filius/protocol';
|
|
import { clientText, serverText } from '../../core/test/programLines.js';
|
|
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
|
|
|
|
interface WindowField {
|
|
readonly text: string;
|
|
readonly editable: boolean;
|
|
}
|
|
|
|
interface JavaStep {
|
|
readonly step: string;
|
|
readonly server: { readonly button: string; readonly port: WindowField; readonly log: string };
|
|
readonly client: { readonly connect: string; readonly sendEnabled: boolean; readonly address: WindowField; readonly port: WindowField; readonly message: string; readonly output: string };
|
|
readonly thrown: readonly string[];
|
|
}
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const java = JSON.parse(readFileSync(join(here, 'echoWindow.java.json'), 'utf8')) as { readonly steps: readonly JavaStep[] };
|
|
const corpus = join(here, '../../../corpus/java-examples');
|
|
|
|
/** The client's own port, drawn by the threads. */
|
|
const ownPort = (text: string) => text.replace(/192\.168\.0\.20:\d+/g, '192.168.0.20:PORT');
|
|
|
|
function setUp() {
|
|
const doc = new Y.Doc();
|
|
writeProject(doc, emptyProject());
|
|
let counter = 0;
|
|
const newId = () => `port-${++counter}`;
|
|
addDevice(doc, { kind: 'pc', id: 'client', name: 'Client', x: 0, y: 0, newId }, 'me');
|
|
addDevice(doc, { kind: 'pc', id: 'server', name: 'Server', x: 200, y: 0, newId }, 'me');
|
|
addDevice(doc, { kind: 'networkSwitch', id: 'switch', name: 'Switch', x: 100, y: 100, newId }, 'me');
|
|
connect(doc, { node: 'client' }, { node: 'switch' }, 'cable-1', 'me');
|
|
connect(doc, { node: 'server' }, { node: 'switch' }, 'cable-2', 'me');
|
|
setHostAddress(doc, 'client', 'ipAddress', '192.168.0.20', 'me');
|
|
setHostAddress(doc, 'server', 'ipAddress', '192.168.0.30', 'me');
|
|
setSoftware(doc, 'server', { install: ['echoServer'], remove: [] }, 'de', 'me');
|
|
setSoftware(doc, 'client', { install: ['simpleClient'], remove: [] }, 'de', 'me');
|
|
let now = 0;
|
|
const room = new SimulationRoom(doc, { now: () => now, seed: () => 3 });
|
|
const views = new Map<string, unknown>();
|
|
const lines = new Map<string, unknown[]>();
|
|
const errors: string[] = [];
|
|
const id = room.connect((message: HostMessage) => {
|
|
if (message.kind === 'view') views.set(message.app, message.view);
|
|
if (message.kind === 'app') lines.set(message.app, [...(lines.get(message.app) ?? []).slice(0, message.from), ...message.lines]);
|
|
if (message.kind === 'error') errors.push(message.code);
|
|
});
|
|
const send = (fields: object) => room.receive(id, { v: 1, ...fields });
|
|
send({ kind: 'sub', topics: ['app:server/echoServer', 'app:client/simpleClient'] });
|
|
send({ kind: 'start' });
|
|
const run = (milliseconds: number) => {
|
|
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
|
|
now += TICK_MILLISECONDS;
|
|
room.tick();
|
|
}
|
|
};
|
|
const act = (node: string, app: string, action: string, args: object = {}) => send({ kind: 'input', node, app, action, args, reqId: action });
|
|
return { doc, room, views, lines, errors, run, act, send };
|
|
}
|
|
|
|
describe("the echo server's and the generic client's windows, against Java's", () => {
|
|
it("play Java's steps as the panels play them", () => {
|
|
const { views, lines, run, act, errors } = setUp();
|
|
run(TICK_MILLISECONDS);
|
|
// What the panels hold: the fields as typed, the message.
|
|
let serverPortText = '';
|
|
let clientAddressText = '';
|
|
let clientPortText = '';
|
|
let message = '';
|
|
const server = () => views.get('echoServer') as EchoServerView;
|
|
const client = () => views.get('simpleClient') as SimpleClientView;
|
|
const serverLog = () => ((lines.get('echoServer') ?? []) as EchoServerLogEntry[]).map((line) => `${serverText(line)}\n`).join('');
|
|
const clientOutput = () => ((lines.get('simpleClient') ?? []) as SimpleClientLogEntry[]).map((line) => `${clientText(line)}\n`).join('');
|
|
// The fields follow the program when it changes (Java's aktualisieren sets them from it).
|
|
let seenServer = server();
|
|
let seenClient = client();
|
|
const follow = () => {
|
|
if (server() !== seenServer) serverPortText = String(server().port);
|
|
if (client() !== seenClient) {
|
|
clientAddressText = client().address;
|
|
clientPortText = String(client().port);
|
|
}
|
|
seenServer = server();
|
|
seenClient = client();
|
|
};
|
|
serverPortText = String(server().port);
|
|
clientAddressText = client().address;
|
|
clientPortText = String(client().port);
|
|
const shown = () => {
|
|
follow();
|
|
return {
|
|
server: { button: server().active ? 'Anhalten' : 'Starten', port: { text: serverPortText, editable: !server().active }, log: ownPort(serverLog()) },
|
|
client: {
|
|
connect: client().connected ? 'Trennen' : 'Verbinden',
|
|
sendEnabled: client().connected,
|
|
address: { text: clientAddressText, editable: !client().connected },
|
|
port: { text: clientPortText, editable: !client().connected },
|
|
message,
|
|
output: clientOutput(),
|
|
},
|
|
};
|
|
};
|
|
// The failed attempt, awaited (see below), is in the window one step before Java's shows it.
|
|
const timedOut = `${clientText({ kind: 'connectFailed', failure: 'timedOut' })}\n`;
|
|
const expected = (step: JavaStep) => ({
|
|
server: { ...step.server, log: ownPort(step.server.log) },
|
|
client: step.step === 'server: start again' ? { ...step.client, output: step.client.output + timedOut } : step.client,
|
|
});
|
|
// The panels' buttons.
|
|
const startStop = () => {
|
|
if (server().active) act('server', 'echoServer', 'setActive', { active: false });
|
|
else if (portOfText(serverPortText) !== undefined) act('server', 'echoServer', 'setActive', { active: true, port: portOfText(serverPortText) });
|
|
};
|
|
const connectButton = () => {
|
|
if (client().connected) act('client', 'simpleClient', 'disconnect');
|
|
else if (portOfText(clientPortText) !== undefined) act('client', 'simpleClient', 'connect', { address: clientAddressText, port: portOfText(clientPortText) });
|
|
};
|
|
const sendButton = () => {
|
|
if (!client().connected) return;
|
|
act('client', 'simpleClient', 'send', { text: message });
|
|
message = '';
|
|
};
|
|
const plays: Record<string, () => void> = {
|
|
'server: start with port abc': () => ((serverPortText = 'abc'), startStop(), run(500)),
|
|
'server: start with port 55555': () => ((serverPortText = '55555'), startStop(), run(1000)),
|
|
'client: connect with port x': () => ((clientAddressText = '192.168.0.30'), (clientPortText = 'x'), connectButton(), run(500)),
|
|
'client: connect to 192.168.0.30:55555': () => ((clientAddressText = '192.168.0.30'), (clientPortText = '55555'), connectButton(), run(6000)),
|
|
'client: send Hallo Echo': () => ((message = 'Hallo Echo'), sendButton(), run(4000)),
|
|
'client: send nothing': () => ((message = ''), sendButton(), run(4000)),
|
|
'client: send two lines': () => ((message = 'Zeile 1\nZeile 2'), sendButton(), run(4000)),
|
|
'server: stop while connected': () => (startStop(), run(4000)),
|
|
'client: send after the server stopped': () => ((message = 'noch da?'), sendButton(), run(8000)),
|
|
'client: Trennen': () => (connectButton(), run(3000)),
|
|
'server: start again': () => (startStop(), run(1000)),
|
|
'client: connect again': () => (connectButton(), run(6000)),
|
|
'client: Trennen again': () => (connectButton(), run(3000)),
|
|
// Three SYNs (F2.3c) last longer than Java's one: until the attempt failed.
|
|
'client: connect to a port nobody listens on': () => {
|
|
clientPortText = '4711';
|
|
connectButton();
|
|
for (let waited = 0; !clientOutput().endsWith(timedOut) && waited < 60_000; waited += 1000) run(1000);
|
|
},
|
|
'client: connect to a name nobody knows': () => ((clientAddressText = 'gibtsnicht'), (clientPortText = '55555'), connectButton(), run(12000)),
|
|
};
|
|
for (const step of java.steps) {
|
|
if (step.step !== 'open') plays[step.step]!();
|
|
expect(shown(), step.step).toEqual(expected(step));
|
|
// The press on "Verbinden" with the server stopped: Java's one SYN times out before the server starts again,
|
|
// the core's three (named in F2.3c) only later — awaited here, so that the server starts after it as in Java.
|
|
if (step.step === 'client: Trennen') {
|
|
for (let waited = 0; !clientOutput().endsWith(timedOut) && waited < 60_000; waited += 1000) run(1000);
|
|
}
|
|
// Java's window threw for a port that is no number; the panel hands nothing over.
|
|
expect(step.thrown.length > 0, step.step).toBe(step.step.includes('port abc') || step.step.includes('port x'));
|
|
}
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
it("leave in the project what Java's leaves after a run: the server's port and that it runs, the client's server", () => {
|
|
let ids = 0;
|
|
const project = openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'email_komplett.fls'))), () => `00000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`);
|
|
const doc = new Y.Doc();
|
|
writeProject(doc, project);
|
|
const id = (name: string) => readProject(doc).graph.nodes.find((node) => node.displayName === name)!.id;
|
|
setSoftware(doc, id('Rechner 2'), { install: ['echoServer'], remove: [] }, 'de', 'me');
|
|
setSoftware(doc, id('Rechner 1'), { install: ['simpleClient'], remove: [] }, 'de', 'me');
|
|
let now = 0;
|
|
const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 });
|
|
const client = room.connect(() => {});
|
|
const act = (node: string, app: string, action: string, args: object = {}) => room.receive(client, { v: 1, kind: 'input', node: id(node), app, action, args, reqId: action });
|
|
const run = (milliseconds: number) => {
|
|
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
|
|
now += TICK_MILLISECONDS;
|
|
room.tick();
|
|
}
|
|
};
|
|
room.receive(client, { v: 1, kind: 'start' });
|
|
run(1000);
|
|
act('Rechner 2', 'echoServer', 'setActive', { active: true, port: 7777 });
|
|
run(1000);
|
|
const address = readProject(doc).deviceConfigurations.get(id('Rechner 2'))!.ipAddress;
|
|
act('Rechner 1', 'simpleClient', 'connect', { address, port: 7777 });
|
|
run(6000);
|
|
act('Rechner 1', 'simpleClient', 'send', { text: 'Hallo' });
|
|
run(4000);
|
|
room.receive(client, { v: 1, kind: 'stop' });
|
|
|
|
let javaIds = 0;
|
|
const saved = openProjectArchive(new Uint8Array(readFileSync(join(here, 'fixtures/after-run-echo.fls'))), () => `00000000-0000-0000-0000-${(++javaIds).toString(16).padStart(12, '0')}`);
|
|
const ours = readProject(doc);
|
|
for (const name of ['Rechner 1', 'Rechner 2']) {
|
|
const of = (each: typeof ours) => {
|
|
const node = each.graph.nodes.find((candidate) => candidate.displayName === name)!.id;
|
|
return { echoServer: each.echoServerConfigurations.get(node), simpleClient: each.simpleClientConfigurations.get(node), active: [...(each.activePrograms.get(node) ?? [])].sort() };
|
|
};
|
|
expect(of(ours), name).toEqual(of(saved));
|
|
}
|
|
});
|
|
|
|
it('refuses what the panels never hand over: a port that is no port, an action the program has not', () => {
|
|
const { run, act, errors, views } = setUp();
|
|
run(TICK_MILLISECONDS);
|
|
act('server', 'echoServer', 'setActive', { active: true, port: 70_000 });
|
|
act('server', 'echoServer', 'setActive', { active: true, port: 1.5 });
|
|
act('server', 'echoServer', 'setActive', { active: 'yes' });
|
|
act('server', 'echoServer', 'setActive', { active: 0 });
|
|
act('server', 'echoServer', 'restart');
|
|
act('client', 'simpleClient', 'connect', { address: '192.168.0.30', port: -1 });
|
|
act('client', 'simpleClient', 'connect', { address: 5, port: 80 });
|
|
act('client', 'simpleClient', 'send', {});
|
|
act('client', 'simpleClient', 'reset');
|
|
act('client', 'echoServer', 'setActive', { active: false });
|
|
act('server', 'simpleClient', 'disconnect');
|
|
expect(errors).toEqual(['badArgs', 'badArgs', 'badArgs', 'badArgs', 'unknownAction', 'badArgs', 'badArgs', 'badArgs', 'unknownAction', 'unknownApp', 'unknownApp']);
|
|
expect([portOfText('0'), portOfText('65535'), portOfText('000080'), portOfText('65536'), portOfText('+5'), portOfText(' 5'), portOfText('123456')]).toEqual([
|
|
0,
|
|
65_535,
|
|
80,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
]);
|
|
// What is handed over is in the view: the port.
|
|
act('server', 'echoServer', 'setActive', { active: true, port: 7777 });
|
|
run(TICK_MILLISECONDS);
|
|
expect(views.get('echoServer')).toEqual({ active: true, port: 7777 });
|
|
});
|
|
});
|