// Programs' threads (threads.ts) and the echo server and generic client // (apps/echo.ts) where echoJava.test.ts's stories do not go: how threads // wake, a taken port, a name, no route, lost data, a new start. import { describe, expect, it } from 'vitest'; import { deviceConfiguration, pause, waitUntil, type NetworkRuntimeEngine, type Steps, type TopologySnapshotInput } from '../src/index.js'; import { link, pc, startedEngine, uuid } from './fixtures.js'; const SERVER = uuid(0x2001); const CLIENT = uuid(0x2002); /** 10.0.0.1 (echo server, DNS server with echo.filius.de) and 10.0.0.2 (client), on one cable. */ function network(changes: Partial = {}): TopologySnapshotInput { return { nodes: [pc(SERVER, uuid(0x2011)), pc(CLIENT, uuid(0x2012))], links: [link(SERVER, uuid(0x2011), CLIENT, uuid(0x2012))], deviceConfigurations: new Map([ [SERVER, deviceConfiguration('10.0.0.1', '255.255.255.0', '', '')], [CLIENT, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '10.0.0.1')], ]), echoServersByNodeId: new Map([[SERVER, { port: 55555, isActive: true }]]), simpleClientsByNodeId: new Map([[CLIENT, { targetAddress: '', targetPort: 55555 }]]), ...changes, }; } const advance = (engine: NetworkRuntimeEngine, toMilliseconds: number) => engine.handle({ type: 'advance', toMilliseconds }); const kinds = (lines: readonly { kind: string }[]) => lines.map((line) => line.kind); describe("programs' threads", () => { it('wake when their condition holds, right after the event that made it so', () => { const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); let flag = false; const woken: number[] = []; engine.startThread( (function* (): Steps { yield* waitUntil(() => flag); woken.push(engine.state.currentTimeMilliseconds); })(), ); engine.handle({ type: 'schedule', deadlineMilliseconds: 300, kind: { kind: 'parityMarker', label: 'a' } }); advance(engine, 200); flag = true; expect(woken).toEqual([]); advance(engine, 1_000); expect(woken).toEqual([300]); }); it('go on after a pause, after the threads that are ready at that time', () => { const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); const order: string[] = []; let ready = false; engine.startThread( (function* (): Steps { yield pause(engine.state.currentTimeMilliseconds); order.push(`paused ${engine.state.currentTimeMilliseconds}`); })(), ); engine.startThread( (function* (): Steps { yield* waitUntil(() => ready); order.push(`waited ${engine.state.currentTimeMilliseconds}`); })(), ); ready = true; engine.resumeThreads(); advance(engine, 0); expect(order).toEqual(['waited 0', 'paused 0']); }); it('wake from a wait by its time only once, not again in a later wait', () => { const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); let flag = false; const woken: string[] = []; engine.startThread( (function* (): Steps { const first = yield* waitUntil(() => flag, 500); woken.push(`first ${first} ${engine.state.currentTimeMilliseconds}`); const second = yield* waitUntil(() => false, 2_000); woken.push(`second ${second} ${engine.state.currentTimeMilliseconds}`); })(), ); engine.handle({ type: 'schedule', deadlineMilliseconds: 100, kind: { kind: 'parityMarker', label: 'a' } }); flag = true; advance(engine, 3_000); // The first wait ended at 100 by its condition; its wake-up at 500 does not end the second. expect(woken).toEqual(['first true 100', 'second false 2000']); }); it('resume in the order they began when the same event wakes them', () => { const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); let flag = false; const order: string[] = []; for (const name of ['first', 'second', 'third']) { engine.startThread( (function* (): Steps { yield* waitUntil(() => flag); order.push(name); })(), ); } flag = true; engine.resumeThreads(); expect(order).toEqual(['first', 'second', 'third']); }); it('do not wait for what already holds: no other thread runs in between', () => { const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); let flag = false; const order: string[] = []; engine.startThread( (function* (): Steps { yield* waitUntil(() => flag); order.push('woken'); })(), ); engine.startThread( (function* (): Steps { flag = true; yield* waitUntil(() => true); order.push('went on'); })(), ); expect(order).toEqual(['went on', 'woken']); }); it('end with the simulation', () => { const engine = startedEngine(network()); expect(engine.echoServer(SERVER)).toBeDefined(); engine.handle({ type: 'stop' }); expect(engine.echoServer(SERVER)).toBeUndefined(); expect(engine.simpleClient(CLIENT)).toBeUndefined(); }); }); describe('the echo server and the generic client', () => { it('switches the server off when its port is taken (sw_serveranwendung_msg6)', () => { const engine = startedEngine(network({ echoServersByNodeId: new Map([[SERVER, { port: 7, isActive: false }]]) })); expect(engine.openTcpServerSocket(SERVER, 7)).toBeDefined(); const server = engine.echoServer(SERVER)!; server.setActive(true); advance(engine, 100); expect(kinds(server.log.all)).toEqual(['acceptingStarted', 'serverSocketFailed', 'acceptingStopped']); expect(server.isActive).toBe(false); }); it('connects to a name through the DNS server', () => { const engine = startedEngine( network({ dnsServerConfigurationsByNodeId: new Map([[SERVER, { recursiveResolutionEnabled: false, records: [{ name: 'echo.filius.de', type: 'A', ttlSeconds: 3600, target: '10.0.0.1' }] }]]), }), ); const client = engine.simpleClient(CLIENT)!; client.connect('echo.filius.de', 55555); advance(engine, 3_000); client.send('Hallo'); advance(engine, 6_000); expect(kinds(client.log.all)).toEqual(['connected', 'sent', 'received']); expect(engine.echoServer(SERVER)!.log.all[0]).toMatchObject({ kind: 'connectionAccepted', address: '10.0.0.2' }); }); it('fails to connect to a name no server knows, without a frame (sw_socket_msg1)', () => { const engine = startedEngine(network({ deviceConfigurations: new Map([[CLIENT, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '')]]) })); const client = engine.simpleClient(CLIENT)!; client.connect('echo.filius.de', 55555); advance(engine, 5_000); expect(client.log.all).toEqual([{ kind: 'connectFailed', failure: 'unresolved', timeMilliseconds: 0 }]); expect(engine.capture.frames(uuid(0x2012))).toEqual([]); }); it('fails at once to connect to an address without a route, without a frame (Java waits for ever)', () => { const engine = startedEngine(network()); const client = engine.simpleClient(CLIENT)!; advance(engine, 100); client.connect('10.9.9.9', 55555); expect(client.log.all).toEqual([{ kind: 'connectFailed', failure: 'failed', timeMilliseconds: 100 }]); advance(engine, 5_000); expect(engine.capture.frames(uuid(0x2012))).toEqual([]); }); it('reports a send that no one acknowledges and the connection it ends (sw_tcpsocket_msg8)', () => { const engine = startedEngine(network()); const client = engine.simpleClient(CLIENT)!; client.connect('10.0.0.1', 55555); advance(engine, 3_000); engine.setGlobalPacketLossEnabled(true); client.send('verloren'); advance(engine, 40_000); expect(client.log.all.filter((line) => line.kind === 'sendFailed')).toMatchObject([{ kind: 'sendFailed', failure: 'timedOut' }]); expect(kinds(client.log.all)).toContain('connectionClosed'); expect(client.isConnected).toBe(false); }); it('starts anew with a new simulation: from the snapshot, with empty windows', () => { const engine = startedEngine(network()); engine.echoServer(SERVER)!.setActive(false); engine.simpleClient(CLIENT)!.send('Hallo'); engine.handle({ type: 'stop' }); engine.handle({ type: 'start', snapshot: engine.state.topologySnapshot, seed: 14, initialTimeMilliseconds: 0 }); expect(engine.echoServer(SERVER)!.isActive).toBe(true); expect(engine.echoServer(SERVER)!.log.all).toEqual([]); expect(engine.simpleClient(CLIENT)!.log.all).toEqual([]); }); it('hands out a message that came before the connection closed (Java drops it)', () => { const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); const listener = engine.openTcpServerSocket(SERVER, 80)!; const client = engine.openTcpClientSocket(CLIENT, '10.0.0.1', 80)!; expect(engine.connectTcpWithResult(client)).toBe('connected'); const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listener)![0]!; advance(engine, engine.state.currentTimeMilliseconds + 100); expect(engine.sendTcp(accepted, new TextEncoder().encode('zuletzt'))).toBe(true); engine.closeTcpSocket(accepted); advance(engine, engine.state.currentTimeMilliseconds + 1_000); expect(engine.state.socketsById.get(client)!.tcpState).not.toBe('ESTABLISHED'); const received: string[] = []; engine.startThread( (function* (): Steps { for (;;) { const next = yield* engine.receiveTcpSteps(client); received.push(next.type === 'message' ? new TextDecoder().decode(next.payload) : next.type); if (next.type !== 'message') return; } })(), ); expect(received).toEqual(['zuletzt', 'notConnected']); }); });