feat(core): the command line with its local commands after Java, measured against Java

The terminal splits a line as Java's window does and runs ls, cd, pwd,
mkdir, touch, cp, mv, rm, cat, test, help, ipconfig and route on the
node's files, which are the project's own. Its texts are generated from
the bundle Java loads. TerminalCases feeds Java's Terminal without a
network (16 cases, de and en); eight cases name Java bugs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:32:35 +00:00
co-authored by Claude Opus 5.5
parent 058675a148
commit 016f9461df
17 changed files with 14404 additions and 7 deletions
+170
View File
@@ -0,0 +1,170 @@
// Feeds FILIUS' command line (Terminal, with the input handling of its window
// GUIApplicationTerminalWindow) with lines as typed, without a running
// network, and writes for each line what the program told the window (each
// text and each Boolean "busy" mark), what it returned, the working directory
// after it, and every file and directory of the host: the reference for the
// core's command line (packages/core/src/runtime/apps/terminal.ts). It also
// writes the terminal's texts of the message bundle, as Java loads them.
// Each case starts on host 10.0.0.2 of a scenario (ScenarioNetwork) with the
// files below and a new Terminal. The cases file is ASCII, with Java-style
// unicode escapes (backslash, u, four hex digits) for anything else:
//
// case <name>
// <line as typed>
//
// Usage: java -cp filius.jar:lib/*:classes TerminalCases <de|en> <scenario.json> <cases txt> <output json>
import filius.rahmenprogramm.Base64;
import filius.rahmenprogramm.Information;
import filius.software.lokal.Terminal;
import filius.software.system.Datei;
import filius.software.system.Dateisystem;
import filius.software.system.InternetKnotenBetriebssystem;
import java.io.FileOutputStream;
import java.lang.reflect.InvocationTargetException;
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.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.TreeMap;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
public class TerminalCases {
static final String HOST = "00000000-0000-0000-0000-000000000301";
/** A tiny PNG (1 x 1 pixel), as FILIUS keeps images: Base64. */
static final String IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
/** The image as FILIUS writes a file's bytes (Base64.encodeBytes, with its line breaks). */
static String imageAsFiliusWritesIt() {
try {
return Base64.encodeBytes(Base64.decode(IMAGE));
} catch (Exception error) {
throw new IllegalStateException(error);
}
}
/** The host's files at the start of each case, in this order. */
static void files(Dateisystem files) {
files.getRoot().removeAllChildren();
files.speicherDatei("/", new Datei("notizen.txt", "txt", "Hallo Welt"));
files.erstelleVerzeichnis("/", "Dokumente");
files.speicherDatei("/Dokumente", new Datei("brief.txt", "txt", "Sehr geehrte Damen und Herren,\nviele Grüße"));
files.erstelleVerzeichnis("/Dokumente", "Alt");
files.speicherDatei("/", new Datei("bild.png", "png", imageAsFiliusWritesIt()));
files.speicherDatei("/", new Datei("kurz.txt", "txt", "SGFsbG8="));
files.erstelleVerzeichnis("/", "leer");
}
public static void main(String[] args) throws Exception {
// Before any class reads the message bundle (I18n.messages is read once).
Information.getInformation().setLocale(args[0].equals("en") ? new Locale("en", "GB") : Locale.GERMANY);
ScenarioNetwork network = ScenarioNetwork.read(args[1]);
InternetKnotenBetriebssystem system = network.system(HOST);
List<Object> cases = new ArrayList<>();
List<Object> steps = null;
Terminal terminal = null;
List<Object> told = new ArrayList<>();
for (String line : Files.readAllLines(Path.of(args[2]), StandardCharsets.US_ASCII)) {
if (line.startsWith("#")) continue;
if (line.startsWith("case ")) {
Map<String, Object> current = new LinkedHashMap<>();
steps = new ArrayList<>();
current.put("name", line.substring("case ".length()));
current.put("steps", steps);
cases.add(current);
files(system.getDateisystem());
system.getInstallierteAnwendungen().remove("filius.software.lokal.Terminal");
system.installApp("filius.software.lokal.Terminal", false);
terminal = (Terminal) system.holeSoftware("filius.software.lokal.Terminal");
terminal.setSystemSoftware(system);
terminal.hinzuBeobachter((observable, data) -> {
if (data instanceof Boolean) told.add(Arrays.asList("busy", data));
else if (data != null) told.add(Arrays.asList("text", data.toString()));
});
current.put("start", tree(system.getDateisystem()));
continue;
}
String typed = IpCheckCases.unescape(line);
Map<String, Object> step = new LinkedHashMap<>();
step.put("typed", typed);
told.clear();
Object returned = null;
String window = null;
// GUIApplicationTerminalWindow: the first word is the command, the others its parameters, then at least three empty ones.
if (typed.trim().isEmpty()) {
window = "blank";
} else {
StringTokenizer tokens = new StringTokenizer(typed, " ");
String command = tokens.nextToken();
String[] parameters = new String[3 + tokens.countTokens()];
Arrays.fill(parameters, "");
for (int index = 0; tokens.hasMoreTokens(); index++) parameters[index] = tokens.nextToken();
if (command.equals("exit")) {
window = "exit";
} else if (command.equals("reset") || command.equals("cls")) {
window = "clear";
} else {
// Terminal.terminalEingabeAuswerten, but in this thread instead of the program's.
try {
Method method = Terminal.class.getDeclaredMethod(command, String[].class);
terminal.setInterrupt(false);
returned = method.invoke(terminal, (Object) parameters);
} catch (NoSuchMethodException error) {
terminal.benachrichtigeBeobachter(terminal.messages.getString("terminal_msg2") + "\n" + terminal.messages.getString("terminal_msg3"));
} catch (InvocationTargetException error) {
returned = "!" + error.getCause().getClass().getSimpleName();
}
}
}
step.put("window", window);
step.put("told", new ArrayList<>(told));
step.put("returned", returned == null ? null : returned.toString());
step.put("directory", Dateisystem.absoluterPfad(terminal.getAktuellerOrdner()));
step.put("files", tree(system.getDateisystem()));
steps.add(step);
}
Map<String, Object> out = new LinkedHashMap<>();
out.put("cases", cases);
ResourceBundle bundle = terminal.messages;
Map<String, Object> texts = new TreeMap<>();
for (String key : Collections.list(bundle.getKeys())) {
if (key.startsWith("sw_terminal") || key.startsWith("terminal_msg")) texts.put(key, bundle.getString(key));
}
out.put("texts", texts);
try (FileOutputStream stream = new FileOutputStream(args[3])) {
stream.write((FlsOracle.Json.write(out, "") + "\n").getBytes(StandardCharsets.UTF_8));
}
System.out.println("terminal cases (" + args[0] + "): " + cases.size());
System.exit(0);
}
/** Every node below the root in preorder: path, and for a file its type, content and size (Datei.holeGroesse). */
static List<Object> tree(Dateisystem files) {
List<Object> nodes = new ArrayList<>();
Enumeration<TreeNode> all = files.getRoot().preorderEnumeration();
while (all.hasMoreElements()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) all.nextElement();
if (node == files.getRoot()) continue;
if (node.getUserObject() instanceof Datei) {
Datei file = (Datei) node.getUserObject();
nodes.add(Arrays.asList(Dateisystem.absoluterPfad(node), file.getDateiTyp(), file.getDateiInhalt(), file.holeGroesse()));
} else {
nodes.add(Arrays.asList(Dateisystem.absoluterPfad(node)));
}
}
return nodes;
}
}