// A program installed or removed while the simulation runs (Java's software // installation): its settings and files from the document's snapshot, the // program started at once; removed, a server is switched off and the program // goes, its files staying. import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { installationFiles, topologySnapshot, VirtualFileSystem, type TopologySnapshot } from '../src/index.js'; import { device, pc, startedEngine, uuid } from './fixtures.js'; const PC = uuid(1); const PORT = uuid(2); /** The document's snapshot after a web server and a command line were installed. */ function source(files: VirtualFileSystem): TopologySnapshot { for (const file of installationFiles('webServer', 'de')) { if (file.content.kind === 'directory') files.createDirectory(file.path); else if (file.content.kind === 'text') files.writeTextFile(file.path, file.content.text); else if (file.content.kind === 'image') files.writeImageFile(file.path, file.content.data, file.content.mediaType); } return topologySnapshot({ nodes: [pc(PC, PORT)], links: [], deviceConfigurations: new Map([[PC, device('10.0.0.1')]]), webServersByNodeId: new Map([[PC, { port: 80, documentRoot: '/webserver', virtualHosts: false, isActive: false }]]), terminalNodeIds: new Set([PC]), fileSystemsByNodeId: new Map([[PC, files]]), }); } describe('a program installed while the simulation runs', () => { it("takes its settings and the files it makes from the document's snapshot, and starts", () => { const own = new VirtualFileSystem(); own.createDirectory('/webserver'); own.writeTextFile('/webserver/index.html', 'Meine Seite'); const engine = startedEngine({ nodes: [pc(PC, PORT)], links: [], deviceConfigurations: new Map([[PC, device('10.0.0.1')]]), fileSystemsByNodeId: new Map([[PC, own]]) }); expect([engine.webServer(PC), engine.terminal(PC)]).toEqual([undefined, undefined]); const from = source(new VirtualFileSystem()); engine.installProgram(PC, 'webServer', from); engine.installProgram(PC, 'commandPrompt', from); expect(engine.webServer(PC)).toBeDefined(); expect(engine.terminal(PC)).toBeDefined(); expect(engine.state.topologySnapshot.webServersByNodeId.get(PC)).toEqual({ port: 80, documentRoot: '/webserver', virtualHosts: false, isActive: false }); // The running files keep what is there; what is missing comes from the document. const files = engine.state.topologySnapshot.fileSystemsByNodeId.get(PC)!; expect(files.textFile('/webserver/index.html')).toBe('Meine Seite'); expect(files.imageFile('/webserver/splashscreen-mini.png').mediaType).toBe('image/png'); // Installed again: nothing changes, the same program runs on. const terminal = engine.terminal(PC); engine.installProgram(PC, 'commandPrompt', from); expect(engine.terminal(PC)).toBe(terminal); }); it('goes when removed, its files staying, and nothing happens outside a run or on a device the run has not', () => { const engine = startedEngine({ nodes: [pc(PC, PORT)], links: [], deviceConfigurations: new Map([[PC, device('10.0.0.1')]]) }); const from = source(new VirtualFileSystem()); engine.installProgram(uuid(9), 'webServer', from); expect(engine.webServer(uuid(9))).toBeUndefined(); engine.installProgram(PC, 'webServer', from); engine.installProgram(PC, 'commandPrompt', from); engine.removeProgram(PC, 'webServer'); engine.removeProgram(PC, 'commandPrompt'); expect([engine.webServer(PC), engine.terminal(PC)]).toEqual([undefined, undefined]); expect(engine.state.topologySnapshot.webServersByNodeId.has(PC)).toBe(false); expect(engine.state.topologySnapshot.terminalNodeIds.has(PC)).toBe(false); expect(engine.state.topologySnapshot.fileSystemsByNodeId.get(PC)!.contains('/webserver/index.html')).toBe(true); engine.handle({ type: 'stop' }); engine.installProgram(PC, 'commandPrompt', from); expect(engine.terminal(PC)).toBeUndefined(); }); }); const templates = join(dirname(fileURLToPath(import.meta.url)), '../../../tools/java-oracle/.upstream/src/main/resources/tmpl'); describe("the files a program makes", () => { it.skipIf(!existsSync(templates))("give the web server Java's start page in the language FILIUS runs in, as Java reads it (each line ended by a line break)", () => { for (const [language, file] of [['de', 'webserver_index_de_DE.txt'], ['en', 'webserver_index_en_GB.txt']] as const) { const lines = readFileSync(join(templates, file), 'utf8').split(/\r?\n/); if (lines.at(-1) === '') lines.pop(); const page = installationFiles('webServer', language).find((each) => each.path === '/webserver/index.html')!; expect(page.content, language).toEqual({ kind: 'text', text: lines.map((line) => `${line}\n`).join('') }); } }); it('are none for a program that makes none', () => { for (const program of ['commandPrompt', 'webBrowser', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient', 'emailClient', 'firewall'] as const) { expect(installationFiles(program, 'de'), program).toEqual([]); } }); }); describe('a server removed while the simulation runs', () => { it('is switched off: its port listens no more', () => { const engine = startedEngine({ nodes: [pc(PC, PORT)], links: [], deviceConfigurations: new Map([[PC, device('10.0.0.1')]]) }); const from = topologySnapshot({ ...source(new VirtualFileSystem()), webServersByNodeId: new Map([[PC, { port: 80, documentRoot: '/webserver', virtualHosts: false, isActive: true }]]) }); engine.installProgram(PC, 'webServer', from); const listening = () => [...engine.state.socketsById.values()].filter((socket) => socket.nodeId === PC && socket.localPort === 80 && socket.tcpState === 'LISTEN'); expect(listening()).toHaveLength(1); engine.removeProgram(PC, 'webServer'); // The server's thread closes the socket, as Java's does, a moment later. engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 100 }); expect(listening()).toEqual([]); }); });