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:
+2
-1
@@ -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** —
|
Swift-Leser aus jeder Datei macht. **Swift ist hier kein Schiedsrichter** —
|
||||||
er verwirft den Java-Gateway unter der Beschriftung `Heimrouter` samt seinen
|
er verwirft den Java-Gateway unter der Beschriftung `Heimrouter` samt seinen
|
||||||
Kabeln und Geräte, deren Position XMLEncoder über eine Rückverweis-Kette
|
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
@@ -14,7 +14,8 @@
|
|||||||
"typecheck": "tsc -b && tsc -p packages/core/test",
|
"typecheck": "tsc -b && tsc -p packages/core/test",
|
||||||
"check": "npm run typecheck && npm run test",
|
"check": "npm run typecheck && npm run test",
|
||||||
"hooks": "git config core.hooksPath scripts/hooks",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.upstream/
|
||||||
|
.build/
|
||||||
@@ -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.
|
||||||
Executable
+31
@@ -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"
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user