test(corpus): Java oracle — FILIUS 2.14 reads the corpus itself

tools/java-oracle builds FILIUS at 6640354 and loads every .fls through
its own private loader under Xvfb. All 23 files load with every node
positioned and every cable resolved (Swift: 20 of 25 nodes in the
Internet example). MAC addresses FILIUS generates are left out, so two
runs are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 19:49:27 +00:00
co-authored by Claude Opus 5.5
parent 8866df5f21
commit 5dcad6e2bf
7 changed files with 9591 additions and 2 deletions
+2 -1
View File
@@ -12,4 +12,5 @@ Projektdateien, an denen Lesen und Schreiben gemessen werden. Alle GPL.
Swift-Leser aus jeder Datei macht. **Swift ist hier kein Schiedsrichter** —
er verwirft den Java-Gateway unter der Beschriftung `Heimrouter` samt seinen
Kabeln und Geräte, deren Position XMLEncoder über eine Rückverweis-Kette
schreibt (s. Plan §13, M2). Schiedsrichter für das Dateiformat ist Java.
schreibt (s. Plan §13, M2). Schiedsrichter für das Dateiformat ist Java: `corpus.java.json` schreibt das
Java-Orakel (`npm run oracle:java`).
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -14,7 +14,8 @@
"typecheck": "tsc -b && tsc -p packages/core/test",
"check": "npm run typecheck && npm run test",
"hooks": "git config core.hooksPath scripts/hooks",
"oracle": "tools/swift-oracle/run.sh"
"oracle": "tools/swift-oracle/run.sh",
"oracle:java": "tools/java-oracle/run.sh"
},
"devDependencies": {
"@types/node": "^20.19.0",
+2
View File
@@ -0,0 +1,2 @@
.upstream/
.build/
+24
View File
@@ -0,0 +1,24 @@
# Java-Orakel
Java-Filius selbst liest den Korpus (`corpus/`) und schreibt, was es gefunden
hat, nach `corpus/corpus.java.json`. **Java ist der Schiedsrichter für das
Dateiformat** — der Swift-Leser verliert in 10 von 23 Dateien Geräte (Plan
§13, M2).
```bash
npm run oracle:java # = tools/java-oracle/run.sh
```
Braucht JDK 17+, Maven und `xvfb-run`. Filius öffnet beim Laden sein
Hauptfenster (`Information.reset` → `JMainFrame`), deshalb läuft es an einem
virtuellen Bildschirm statt „headless".
* Gelesen wird über Filius' eigene, private Lademethode
`SzenarioVerwaltung.netzwerkLaden` (per Reflection) — Javas Weg, kein Nachbau.
* Stand: `gitlab.com/filius1/filius` @ `6640354` (2.14.0), wird beim ersten
Lauf nach `.upstream/` geholt und gebaut.
* **MAC-Adressen**, die nicht in der Datei stehen, würfelt Filius beim Anlegen
der Netzwerkkarte. Sie erscheinen als `null`; zwei Läufe sind byte-gleich.
* Bisher ausgelesen: Geräte (Klasse, Name, Beschriftung, Position,
Schnittstellen), Kabel (Enden als Gerät/Schnittstelle), Doku-Elemente.
Systemsoftware, Dateisysteme und Anwendungen folgen schrittweise.
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh
# Reads every .fls under corpus/ with Java FILIUS itself and writes
# corpus/corpus.java.json. Java is the arbiter for the file format (plan §13, M2).
# Needs a JDK (17+), Maven and xvfb-run: FILIUS opens its main window while
# loading (Information.reset → JMainFrame), so it needs a display, not headless.
set -eu
here="$(cd "$(dirname "$0")" && pwd)"
root="$(cd "$here/../.." && pwd)"
commit=6640354
upstream="$here/.upstream"
if [ ! -d "$upstream/.git" ]; then
git clone -q https://gitlab.com/filius1/filius.git "$upstream"
fi
if [ "$(git -C "$upstream" rev-parse --short=7 HEAD)" != "$commit" ]; then
git -C "$upstream" fetch -q origin
git -C "$upstream" checkout -q "$commit"
rm -rf "$upstream/target"
fi
if [ ! -f "$upstream/target/filius.jar" ]; then
(cd "$upstream" && mvn -q -DskipTests package dependency:copy-dependencies -DoutputDirectory=target/lib)
fi
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
mkdir -p "$here/.build"
javac -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java"
# FILIUS writes its settings below $HOME; keep them out of the real one.
mkdir -p "$here/.build/home"
# Removed first: a crashed run must not leave the previous result standing.
rm -f "$root/corpus/corpus.java.json"
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" FlsOracle \
"$root/corpus" "$root/corpus/corpus.java.json" 2>&1 | grep -v 'logback\|^$' || true
test -s "$root/corpus/corpus.java.json"
+231
View File
@@ -0,0 +1,231 @@
// Reads .fls files the way Java FILIUS does and writes what it found.
// Uses FILIUS's own private loader (SzenarioVerwaltung.netzwerkLaden) by
// reflection, so the result is Java's view, not a re-implementation of it.
//
// Usage: java -Djava.awt.headless=true -cp filius.jar:lib/*:classes FlsOracle <corpus dir> <output json>
import filius.gui.netzwerksicht.GUIDocuItem;
import filius.gui.netzwerksicht.GUIKabelItem;
import filius.gui.netzwerksicht.GUIKnotenItem;
import filius.hardware.Kabel;
import filius.hardware.NetzwerkInterface;
import filius.hardware.Port;
import filius.hardware.knoten.InternetKnoten;
import filius.hardware.knoten.Knoten;
import filius.hardware.knoten.LokalerKnoten;
import filius.rahmenprogramm.SzenarioVerwaltung;
import java.awt.Color;
import java.awt.Font;
import java.awt.Rectangle;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class FlsOracle {
public static void main(String[] args) throws Exception {
Path root = Path.of(args[0]);
List<Path> files;
try (Stream<Path> walk = Files.walk(root)) {
files = walk.filter(p -> p.toString().endsWith(".fls")).sorted().toList();
}
Map<String, Object> result = new TreeMap<>();
for (Path file : files) {
String name = root.relativize(file).toString();
try {
result.put(name, read(file));
} catch (Throwable error) {
Throwable cause = error instanceof java.lang.reflect.InvocationTargetException && error.getCause() != null
? error.getCause()
: error;
Map<String, Object> failure = new LinkedHashMap<>();
failure.put("error", cause.toString());
List<Object> frames = new ArrayList<>();
for (StackTraceElement frame : cause.getStackTrace()) {
if (frames.size() == 8) break;
frames.add(frame.toString());
}
failure.put("at", frames);
result.put(name, failure);
}
}
try (FileOutputStream out = new FileOutputStream(args[1])) {
out.write((Json.write(result, "") + "\n").getBytes(StandardCharsets.UTF_8));
}
System.out.println("corpus: " + result.size() + " files");
// FILIUS leaves non-daemon threads behind; the JVM would never end.
System.exit(0);
}
@SuppressWarnings("unchecked")
static Map<String, Object> read(Path file) throws Exception {
Path xml = Files.createTempFile("konfiguration", ".xml");
try {
extractConfiguration(file, xml);
List<GUIKnotenItem> nodes = new ArrayList<>();
List<GUIKabelItem> cables = new ArrayList<>();
List<GUIDocuItem> docs = new ArrayList<>();
Method load = SzenarioVerwaltung.class.getDeclaredMethod("netzwerkLaden", String.class, List.class,
List.class, List.class);
load.setAccessible(true);
boolean success = (Boolean) load.invoke(null, xml.toString(), nodes, cables, docs);
return describe(success, nodes, cables, docs, Files.readString(xml, StandardCharsets.UTF_8));
} finally {
Files.deleteIfExists(xml);
}
}
static void extractConfiguration(Path archive, Path target) throws Exception {
try (ZipInputStream zip = new ZipInputStream(Files.newInputStream(archive))) {
for (ZipEntry entry; (entry = zip.getNextEntry()) != null;) {
if (entry.getName().equals("projekt/konfiguration.xml")) {
Files.copy(zip, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
return;
}
}
}
throw new IllegalStateException("no projekt/konfiguration.xml");
}
static Map<String, Object> describe(boolean success, List<GUIKnotenItem> nodes, List<GUIKabelItem> cables,
List<GUIDocuItem> docs, String xmlText) {
Map<Port, int[]> portOwners = new IdentityHashMap<>();
List<Object> nodeList = new ArrayList<>();
for (int nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) {
GUIKnotenItem item = nodes.get(nodeIndex);
Knoten node = item.getKnoten();
Map<String, Object> json = new LinkedHashMap<>();
json.put("class", node.getClass().getSimpleName());
json.put("name", node.getName());
json.put("label", item.getImageLabel() == null ? null : item.getImageLabel().getTyp());
Rectangle bounds = item.getImageLabel() == null ? null : item.getImageLabel().getBounds();
json.put("bounds", bounds == null ? null : List.of(bounds.x, bounds.y, bounds.width, bounds.height));
List<Object> ports = new ArrayList<>();
if (node instanceof InternetKnoten internet) {
List<NetzwerkInterface> nics = internet.getNetzwerkInterfaces();
for (int index = 0; index < nics.size(); index++) {
NetzwerkInterface nic = nics.get(index);
Map<String, Object> port = new LinkedHashMap<>();
port.put("ip", nic.getIp());
port.put("mask", nic.getSubnetzMaske());
// Without a MAC address in the file, FILIUS rolls a random one when it
// creates the interface; only a MAC that stands in the file is stable.
String mac = nic.getMac();
port.put("mac", mac != null && xmlText.contains(">" + mac + "<") ? mac : null);
port.put("gateway", nic.getGateway());
port.put("dns", nic.getDns());
ports.add(port);
if (nic.getPort() != null) portOwners.put(nic.getPort(), new int[] { nodeIndex, index });
}
} else if (node instanceof LokalerKnoten local) {
List<Port> anschluesse = local.getAnschluesse();
for (int index = 0; index < anschluesse.size(); index++) {
ports.add(Map.of());
portOwners.put(anschluesse.get(index), new int[] { nodeIndex, index });
}
}
json.put("ports", ports);
nodeList.add(json);
}
List<Object> cableList = new ArrayList<>();
for (GUIKabelItem item : cables) {
Kabel cable = item.getDasKabel();
Map<String, Object> json = new LinkedHashMap<>();
List<Object> ends = new ArrayList<>();
Port[] anschluesse = cable == null ? null : cable.getAnschluesse();
if (anschluesse != null) {
for (Port port : anschluesse) {
int[] owner = port == null ? null : portOwners.get(port);
ends.add(owner == null ? null : List.of(owner[0], owner[1]));
}
}
json.put("ends", ends);
json.put("wireless", cable != null && cable.getWireless());
cableList.add(json);
}
List<Object> docList = new ArrayList<>();
for (GUIDocuItem item : docs) {
Map<String, Object> json = new LinkedHashMap<>();
json.put("type", item.getType());
json.put("text", item.getText());
json.put("bounds", List.of(item.getX(), item.getY(), item.getWidth(), item.getHeight()));
Color color = item.getColor();
json.put("color", color == null ? null : List.of(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
Font font = item.getFont();
json.put("font", font == null ? null : List.of(font.getName(), font.getSize(), font.getStyle()));
docList.add(json);
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", success);
result.put("nodes", nodeList);
result.put("cables", cableList);
result.put("documentation", docList);
return result;
}
/** Just enough JSON: maps, lists, strings, numbers, booleans, null. */
static final class Json {
static String write(Object value, String indent) {
if (value == null) return "null";
if (value instanceof String text) return quote(text);
if (value instanceof Number || value instanceof Boolean) return value.toString();
String inner = indent + " ";
if (value instanceof Map<?, ?> map) {
if (map.isEmpty()) return "{}";
StringBuilder out = new StringBuilder("{\n");
int count = 0;
for (Map.Entry<?, ?> entry : map.entrySet()) {
out.append(inner).append(quote(entry.getKey().toString())).append(": ")
.append(write(entry.getValue(), inner));
out.append(++count < map.size() ? ",\n" : "\n");
}
return out.append(indent).append("}").toString();
}
if (value instanceof List<?> list) {
if (list.isEmpty()) return "[]";
StringBuilder out = new StringBuilder("[\n");
for (int index = 0; index < list.size(); index++) {
out.append(inner).append(write(list.get(index), inner));
out.append(index + 1 < list.size() ? ",\n" : "\n");
}
return out.append(indent).append("]").toString();
}
throw new IllegalArgumentException("not JSON: " + value.getClass());
}
static String quote(String text) {
StringBuilder out = new StringBuilder("\"");
for (char c : text.toCharArray()) {
switch (c) {
case '"' -> out.append("\\\"");
case '\\' -> out.append("\\\\");
case '\n' -> out.append("\\n");
case '\r' -> out.append("\\r");
case '\t' -> out.append("\\t");
default -> {
if (c < 0x20) out.append(String.format("\\u%04x", (int) c));
else out.append(c);
}
}
}
return out.append('"').toString();
}
}
}