feat(editor): F4.6c the software installation on a computer's desktop
A new oracle, InstallRun, installs each of Java's programs on new computers, all of them on one, each installed and removed again, and a DNS and an e-mail server installed again after their removal; Java saves the result as a project. A new program is switched off (a computer's firewall is on), makes the folders and files it lacks (the web server's start page in FILIUS' language and its picture, /peer2peer, /mailserver/konten.txt, /dns/hosts), and removing it takes its settings and leaves its files; a server installed again reads its records or accounts from them. Java installs in the order of its list and starts the program at once. The document's setSoftware does the same, checked against Java's saved installation. The core knows what a program makes in the files and installs or removes a program while the simulation runs; the room watches the document's programs and hands changes to the engine, so a new command line works at once. The desktop offers Java's dialog with two lists, arrows, double click and apply changes. Also: Java's file may name a number it rolled by the result of any getter (an idref, here a thread's priority); GetterDefaults now measures the priority of the saved thread classes, which the import read as 0 before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,162 @@
|
||||
// The software installation against Java's (tools/java-oracle InstallRun,
|
||||
// fixtures/installed-programs.fls): each program installed on a new computer,
|
||||
// all on one, each installed and removed again, a DNS server and an e-mail
|
||||
// server installed again where their removed ones left a record and an
|
||||
// account — the same programs, settings and files as Java saved, read with
|
||||
// the same reader. Named: Java's Gnutella rolls its number of peers.
|
||||
|
||||
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 { Id, InstallableProgram, VirtualFileSystem } from '@mdg-filius/core';
|
||||
import { openProjectArchive, PROGRAM_OF_CLASS, saveProjectArchive, type ImportedProject } from '@mdg-filius/fls';
|
||||
import { addDevice, createUndoManager, emptyProject, installedSoftware, NEW_GNUTELLA_PEERS, readProject, ROOTS, setSoftware, writeProject, type Software } from '../src/index.js';
|
||||
|
||||
const fixture = join(dirname(fileURLToPath(import.meta.url)), 'fixtures/installed-programs.fls');
|
||||
let counter = 0;
|
||||
const java = openProjectArchive(new Uint8Array(readFileSync(fixture)), () => `90000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||
const PROGRAMS = Object.entries(PROGRAM_OF_CLASS).map(([javaClass, program]) => [javaClass.slice(javaClass.lastIndexOf('.') + 1), program as Software] as const);
|
||||
|
||||
/** A new PC in a new document. */
|
||||
function computer() {
|
||||
const doc = new Y.Doc();
|
||||
writeProject(doc, emptyProject());
|
||||
let ports = 0;
|
||||
addDevice(doc, { kind: 'pc', id: 'pc', name: 'PC', x: 0, y: 0, newId: () => `port-${++ports}` }, 'me');
|
||||
return doc;
|
||||
}
|
||||
|
||||
/** Folders and files, each with its content: text as it is, pictures by kind and size. */
|
||||
function filesOf(files: VirtualFileSystem | undefined): string[] {
|
||||
const listed: string[] = [];
|
||||
const walk = (folder: string) => {
|
||||
for (const entry of files?.entriesInCreationOrder(folder) ?? []) {
|
||||
const content = entry.content;
|
||||
listed.push(`${entry.path} ${content.kind === 'text' ? JSON.stringify(content.text) : content.kind === 'directory' ? '/' : `${content.kind} ${content.data.length}`}`);
|
||||
if (content.kind === 'directory') walk(entry.path);
|
||||
}
|
||||
};
|
||||
walk('/');
|
||||
return listed;
|
||||
}
|
||||
|
||||
/** What a computer holds of its programs, as the model has it. */
|
||||
function slice(project: ImportedProject, id: Id) {
|
||||
const gnutella = project.gnutellaConfigurations.get(id);
|
||||
return {
|
||||
programs: [...(project.installedPrograms.get(id) ?? [])].sort(),
|
||||
active: [...(project.activePrograms.get(id) ?? [])],
|
||||
webServer: project.webServerConfigurations.get(id),
|
||||
echoServer: project.echoServerConfigurations.get(id),
|
||||
simpleClient: project.simpleClientConfigurations.get(id),
|
||||
dnsServer: project.dnsServerConfigurations.get(id),
|
||||
emailServer: project.emailServerConfigurations.get(id),
|
||||
emailClient: project.emailClientConfigurations.get(id),
|
||||
// Java rolls it (3 to 5), and its file may name it by any number that came before (an idref).
|
||||
gnutella: gnutella === undefined ? undefined : { maximumKnownPeers: NEW_GNUTELLA_PEERS },
|
||||
firewall: project.firewallConfigurationsByNodeId.get(id),
|
||||
files: filesOf(project.virtualFileSystems.get(id)),
|
||||
};
|
||||
}
|
||||
|
||||
const javaComputer = (name: string) => java.graph.nodes.find((node) => node.displayName === name)!.id;
|
||||
|
||||
describe("the software installation, against Java's", () => {
|
||||
it('installs each program with its settings and files', () => {
|
||||
for (const [name, program] of PROGRAMS) {
|
||||
const doc = computer();
|
||||
setSoftware(doc, 'pc', { install: [program], remove: [] }, 'de', 'me');
|
||||
expect(slice(readProject(doc), 'pc'), name).toEqual(slice(java, javaComputer(name)));
|
||||
}
|
||||
});
|
||||
|
||||
it('installs all programs on one computer, in the order of Java’s list', () => {
|
||||
const doc = computer();
|
||||
// Taken on in any order: the dialog installs them in the order of its list.
|
||||
setSoftware(doc, 'pc', { install: PROGRAMS.map(([, program]) => program).reverse(), remove: [] }, 'de', 'me');
|
||||
expect(slice(readProject(doc), 'pc')).toEqual(slice(java, javaComputer('Alle')));
|
||||
});
|
||||
|
||||
it('removes a program with its settings, its files staying', () => {
|
||||
for (const [name, program] of PROGRAMS) {
|
||||
const doc = computer();
|
||||
setSoftware(doc, 'pc', { install: [program], remove: [] }, 'de', 'me');
|
||||
setSoftware(doc, 'pc', { install: [], remove: [program] }, 'de', 'me');
|
||||
expect(slice(readProject(doc), 'pc'), name).toEqual(slice(java, javaComputer(`Entfernt-${name}`)));
|
||||
}
|
||||
});
|
||||
|
||||
it('reads the records and accounts a removed server left, when it is installed again', () => {
|
||||
for (const [name, program, path, text] of [
|
||||
['DNSServer', 'dnsServer', '/dns/hosts', 'www.filius.de. A 3600 1.2.3.4\n'],
|
||||
['EmailServer', 'emailServer', '/mailserver/konten.txt', 'anna;filius.de;geheim;Muster;Anna;\n'],
|
||||
] as const) {
|
||||
const doc = computer();
|
||||
setSoftware(doc, 'pc', { install: [program], remove: [] }, 'de', 'me');
|
||||
// What Java's server wrote when it got the record or the account (its windows follow in F4.6g).
|
||||
const files = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('files') as Y.Map<Y.Map<unknown>>;
|
||||
(files.get(path)!.get('text') as Y.Text).insert(0, text);
|
||||
setSoftware(doc, 'pc', { install: [], remove: [program] }, 'de', 'me');
|
||||
setSoftware(doc, 'pc', { install: [program], remove: [] }, 'de', 'me');
|
||||
expect(slice(readProject(doc), 'pc'), name).toEqual(slice(java, javaComputer(`Wieder-${name}`)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('the software installation', () => {
|
||||
it("writes the web server's start page in the language FILIUS runs in, and keeps a page that is there", () => {
|
||||
const doc = computer();
|
||||
setSoftware(doc, 'pc', { install: ['webServer'], remove: [] }, 'en', 'me');
|
||||
const files = () => readProject(doc).virtualFileSystems.get('pc')!;
|
||||
const page = files().entriesInCreationOrder('/webserver')[0]!;
|
||||
expect(page.content.kind === 'text' && page.content.text).toContain('<p>Welcome at FILIUS!</p>');
|
||||
// A page of its own: installed again, the server keeps it and brings no picture.
|
||||
const entries = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('files') as Y.Map<Y.Map<unknown>>;
|
||||
setSoftware(doc, 'pc', { install: [], remove: ['webServer'] }, 'en', 'me');
|
||||
entries.delete('/webserver/splashscreen-mini.png');
|
||||
(entries.get('/webserver/index.html')!.get('text') as Y.Text).insert(0, 'Meine Seite ');
|
||||
setSoftware(doc, 'pc', { install: ['webServer'], remove: [] }, 'de', 'me');
|
||||
expect(filesOf(files()).filter((each) => each.startsWith('/webserver'))).toEqual(['/webserver /', expect.stringMatching(/^\/webserver\/index\.html "Meine Seite <html>/)]);
|
||||
});
|
||||
|
||||
it('installs nothing twice, removes nothing not there, takes one step of the undo, and installs only on computers', () => {
|
||||
const doc = computer();
|
||||
const undo = createUndoManager(doc, 'me');
|
||||
setSoftware(doc, 'pc', { install: ['commandPrompt', 'webServer'], remove: ['textEditor'] }, 'de', 'me');
|
||||
expect([...installedSoftware(doc, 'pc')!].sort()).toEqual(['commandPrompt', 'webServer']);
|
||||
const before = readProject(doc);
|
||||
setSoftware(doc, 'pc', { install: ['webServer'], remove: [] }, 'de', 'me');
|
||||
expect(slice(readProject(doc), 'pc')).toEqual(slice(before, 'pc'));
|
||||
undo.undo();
|
||||
expect(installedSoftware(doc, 'pc')).toEqual([]);
|
||||
expect(filesOf(readProject(doc).virtualFileSystems.get('pc'))).toEqual([]);
|
||||
let ports = 0;
|
||||
addDevice(doc, { kind: 'router', id: 'router', name: 'R', x: 0, y: 0, newId: () => `r-${++ports}` }, 'me');
|
||||
setSoftware(doc, 'router', { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||
expect(installedSoftware(doc, 'router')).toBeUndefined();
|
||||
expect(readProject(doc).installedPrograms.has('router')).toBe(false);
|
||||
setSoftware(doc, 'nobody', { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||
});
|
||||
|
||||
it("gives a new Gnutella 3 peers, as Java's file without the number reads (Java rolls 3 to 5)", () => {
|
||||
const doc = computer();
|
||||
setSoftware(doc, 'pc', { install: ['gnutella'], remove: [] }, 'de', 'me');
|
||||
expect(readProject(doc).gnutellaConfigurations.get('pc')).toEqual({ maximumKnownPeers: 3 });
|
||||
let ids = 0;
|
||||
const reopened = openProjectArchive(saveProjectArchive(readProject(doc)).archive, () => `70000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`);
|
||||
expect(reopened.gnutellaConfigurations.get(reopened.graph.nodes[0]!.id)).toEqual({ maximumKnownPeers: 3 });
|
||||
});
|
||||
|
||||
it('goes into the file as Java would read it', () => {
|
||||
const doc = computer();
|
||||
setSoftware(doc, 'pc', { install: ['webServer', 'emailServer', 'commandPrompt'], remove: [] }, 'de', 'me');
|
||||
const project = readProject(doc);
|
||||
expect(project.installedPrograms.get('pc')).toEqual(new Set<InstallableProgram>(['webServer', 'emailServer', 'commandPrompt']));
|
||||
let ids = 0;
|
||||
const reopened = openProjectArchive(saveProjectArchive(project).archive, () => `80000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`);
|
||||
const again = reopened.graph.nodes[0]!.id;
|
||||
expect(slice(reopened, again)).toEqual(slice(project, 'pc'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user