Files
mdg-filius/packages/fls/test/topologyWriter.test.ts
T
julian.kiedaischandClaude Opus 5.5 314f696515 feat(fls): write the topology of a .fls, loaded by Java as the original
topologyWriter.ts writes konfiguration.xml from Java's view of a project:
the version line, the node items (label position, device with name,
host label flags, interfaces or ports), the cable items and the
documentation items. What a device's constructor makes is changed through
getters, as XMLEncoder does, and more is added; fewer than it makes set the
list anew. Cable ends name the port objects the getters return (the writer
now names a getter's result, <void id="Port0" property="port"/>), and each
cable carries the nodes the editor draws it between.

Checked by Java against Java: every corpus file, read and written again,
loads in Java FILIUS (the oracle's loading method, --sha256 ties its result
to the written bytes) to the same devices, names, positions, interfaces,
cables and documentation as the original; so does a synthetic topology with
what the corpus never sets. The oracle and the reader now also report a
cable's drawing ends (JCablePanel ziel1/ziel2): in the corpus the nodes it
joins, for all 183 cables. Mutation probes rewrite the files and let Java
load each mutant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:04:21 +00:00

155 lines
6.4 KiB
TypeScript

// 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<string, JavaFile>;
const written = JSON.parse(readFileSync(join(here, 'written.java.json'), 'utf8')) as Record<string, JavaFile>;
/** 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 }],
});
});
});