// Every corpus file, read and written again: Java FILIUS loads the written // file (tools/java-oracle, written.java.json) to the same topology as the // original (corpus.java.json) — devices, names, positions, interfaces, // cables and documentation. Java against Java: no mapping of this package // stands between the two. // // UPDATE_WRITTEN=1 writes the files to test/written/ (not checked in); the // Java oracle then loads them and records each file's SHA-256, which must // match what the writer makes now. import { createHash } from 'node:crypto'; import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { decodeJavaBeans, readFlsArchive, readTopology, writeFlsArchive } from '../src/index.js'; import { writeTopologyXml, type TopologyToWrite } from '../src/topologyWriter.js'; const here = dirname(fileURLToPath(import.meta.url)); const corpusRoot = join(here, '../../../corpus'); const writtenRoot = join(here, 'written'); function flsFiles(directory: string): string[] { return readdirSync(directory).flatMap((name) => { const path = join(directory, name); if (statSync(path).isDirectory()) return flsFiles(path); return name.endsWith('.fls') ? [path] : []; }); } type JavaNode = { class: string; name: string | null; bounds: number[]; useIpAsName: boolean; useMacAsName: boolean; ports: unknown }; type JavaFile = { success: boolean; nodes: JavaNode[]; cables: unknown[]; documentation: unknown[]; sha256?: string }; const original = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record; const written = JSON.parse(readFileSync(join(here, 'written.java.json'), 'utf8')) as Record; /** The topology of Java's view: the label's position, not its size, which Java works out itself. */ function topologyOf(view: JavaFile) { return { success: view.success, nodes: view.nodes.map((node) => ({ class: node.class, name: node.name, position: node.bounds.slice(0, 2), useIpAsName: node.useIpAsName, useMacAsName: node.useMacAsName, ports: node.ports, })), cables: view.cables, documentation: view.documentation, }; } describe('the topology written again, as Java loads it', () => { for (const file of flsFiles(corpusRoot)) { const name = relative(corpusRoot, file); it(name, () => { const topology = readTopology(decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml)); const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(topology))); if (process.env.UPDATE_WRITTEN === '1') { mkdirSync(dirname(join(writtenRoot, name)), { recursive: true }); writeFileSync(join(writtenRoot, name), archive); } const java = written[name]; expect(java?.sha256, 'written.java.json is not about this output: UPDATE_WRITTEN=1, then npm run oracle:java').toBe( createHash('sha256').update(archive).digest('hex'), ); expect(topologyOf(java!)).toEqual(topologyOf(original[name]!)); }); } }); /** What the corpus does not have: a name set to null, useMACAsName, fewer ports than the constructor makes, more interfaces. */ const SYNTHETIC: TopologyToWrite = { nodes: [ { javaClass: 'Rechner', name: null, x: 10, y: 20, useIpAsName: false, useMacAsName: true, interfaces: [{ ip: '10.0.0.1', mask: '255.0.0.0', mac: 'AA:BB:CC:DD:EE:01', gateway: null, dns: '10.0.0.53' }], portCount: 1, }, { javaClass: 'Switch', name: 'S', x: 100, y: 20, useIpAsName: false, useMacAsName: false, interfaces: [], portCount: 3 }, { javaClass: 'Vermittlungsrechner', name: 'R', x: 200, y: 20, useIpAsName: false, useMacAsName: false, interfaces: ['1', '2', '3'].map((octet) => ({ ip: `10.0.${octet}.1`, mask: '255.255.255.0', mac: `AA:BB:CC:DD:EE:1${octet}`, gateway: '', dns: '', })), portCount: 3, }, { javaClass: 'Modem', name: 'M', x: 300, y: 20, useIpAsName: false, useMacAsName: false, interfaces: [], portCount: 1 }, ], links: [ { ends: [{ node: 0, port: 0 }, { node: 1, port: 2 }], wireless: false, panel: [0, 1] }, { ends: [{ node: 2, port: 2 }, { node: 1, port: 0 }], wireless: true, panel: [2, 1] }, ], documentation: [{ type: 1, text: null, x: 5, y: 6, width: 70, height: 80, color: null, font: null }], }; describe('a topology beyond the corpus, as Java loads it', () => { it('keeps what the corpus never sets', () => { const name = 'synthetic/synthetic.fls'; const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(SYNTHETIC))); if (process.env.UPDATE_WRITTEN === '1') { mkdirSync(join(writtenRoot, 'synthetic'), { recursive: true }); writeFileSync(join(writtenRoot, name), archive); } const java = written[name]; expect(java?.sha256, 'written.java.json is not about this output: UPDATE_WRITTEN=1, then npm run oracle:java').toBe( createHash('sha256').update(archive).digest('hex'), ); const plainInterface = (octet: string) => ({ ip: `10.0.${octet}.1`, mask: '255.255.255.0', mac: `AA:BB:CC:DD:EE:1${octet}`, gateway: '', dns: '' }); expect(topologyOf(java!)).toEqual({ success: true, nodes: [ { class: 'Rechner', name: null, position: [10, 20], useIpAsName: false, useMacAsName: true, ports: [{ ip: '10.0.0.1', mask: '255.0.0.0', mac: 'AA:BB:CC:DD:EE:01', gateway: null, dns: '10.0.0.53' }], }, { class: 'Switch', name: 'S', position: [100, 20], useIpAsName: undefined, useMacAsName: undefined, ports: [{}, {}, {}] }, { class: 'Vermittlungsrechner', name: 'R', position: [200, 20], useIpAsName: undefined, useMacAsName: undefined, ports: ['1', '2', '3'].map(plainInterface), }, { class: 'Modem', name: 'M', position: [300, 20], useIpAsName: undefined, useMacAsName: undefined, ports: [{}] }, ], cables: [ { ends: [[0, 0], [1, 2]], wireless: false, panel: [0, 1] }, { ends: [[2, 2], [1, 0]], wireless: true, panel: [2, 1] }, ], documentation: [{ type: 1, text: null, bounds: [5, 6, 70, 80], color: null, font: null }], }); }); });