test: F3 acceptance — the classroom classics on FILIUS' examples against Java
A web page by its DNS name (internetwork_komplett), an e-mail between two networks through NAT and MX relay (Internet_Komplett_..._DE), a file found and fetched with Gnutella across a router (zwei_rechnernetze_komplett): ExampleRun records frames, tables, program lines and end states; the core plays the same calls and matches card by card. Equal RIP ties take Java's choice, RIP beacons are left out, the search's two connections compared flow by flow. Also: a core test that an equal RIP route from another router neither replaces nor refreshes the route, and nine stale type errors in core tests fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -50,7 +50,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
||||
| `test/dhcpJava.test.ts`, `test/scenarios/*.java.json`, `test/scenario.ts`, `test/javaRun.ts` | — | neu: die DHCP-Szenarien gegen Java-Filius (`tools/java-oracle`, `DhcpRun`): je Client dieselben Frames in derselben Reihenfolge, Abstände auf 200 ms, dieselben Adressen am Ende |
|
||||
| `test/arpJava.test.ts`, `test/scenarios/arp-routed.scenario.json`, `test/scenarios/arp-java-*.java.json` | — | neu: vier ARP-Abläufe gegen Java-Filius (`ArpRun`): Frames, Abstände und wann jeder Aufruf zurückkehrt; die zwei Pings stimmen seit F2.4b-1, die zwei UDP-Abläufe seit F2.4b-2 (Javas Transport-Thread), drei Abläufe durch den Router seit F2.4b-3 (IP- und ICMP-Thread) |
|
||||
| `test/natJava.test.ts`, `test/scenarios/nat-home.scenario.json`, `test/scenarios/nat-java-*.java.json` | — | neu: sechs NAT-Abläufe gegen Java-Filius (`NatRun`): je Absenderkarte dieselben Frames in derselben Folge zur selben Zeit, dieselben Aufrufe, dieselbe NAT-Tabelle; freie Ports und TCP-Startnummern als Platzhalter; ein benannter Java-Fehler (`JAVA_BUGS`: die Antwort der zweiten Gegenstelle) |
|
||||
| `test/javaRun.ts` (`expectSameCaptures`) | — | seit F2.8a: der Mitschnitt jeder Karte gegen Javas Liste dieser Karte (dieselben Frames, dieselbe Folge, dieselbe Zeit), in den Vergleichen für ARP, WLAN, DNS, TCP, NAT, RIP und DHCP mit einem Client; seit F2.8b für jeden dieser Frames auch Javas Tabellenzeilen in beiden Ansichten (Wörter und Zahlen wie Javas deutsches Gebietsschema) |
|
||||
| `test/javaRun.ts` (`expectSameCaptures`) | — | seit F2.8a: der Mitschnitt jeder Karte gegen Javas Liste dieser Karte (dieselben Frames, dieselbe Folge, dieselbe Zeit), in den Vergleichen für ARP, WLAN, DNS, TCP, NAT, RIP und DHCP mit einem Client; seit F2.8b für jeden dieser Frames auch Javas Tabellenzeilen in beiden Ansichten (Wörter und Zahlen wie Javas deutsches Gebietsschema); seit der Abnahme F3 wahlweise Fluss für Fluss (`flow`): wo Frames verschiedener Verbindungen in beliebiger Folge kommen, je Fluss in Folge, mit Zeit und Tabellenzeilen; und nur ausgewählte Frames (`compared`) |
|
||||
| `test/echoJava.test.ts`, `test/scenarios/echo-java-*.java.json` | — | neu (F3.1): acht Abläufe mit Echo-Server und einfachem Client gegen Java-Filius (`EchoRun`): je Karte Frames, Zeiten und Tabellenzeilen, dazu die Fensterzeilen beider Programme auf Deutsch wie Java (Zeilen, die Java erst bei seinem 100-ms-Blick schreibt, dürfen dort bis zu 100 ms später kommen); drei benannte Java-Fehler (`JAVA_BUGS`: keine Route, nur ein SYN, das unbeantwortete FIN nach dem Ausschalten) |
|
||||
| `test/webJava.test.ts`, `test/scenarios/web-lan.scenario.json`, `test/scenarios/web-java-*.java.json`, `test/programLines.ts` | — | neu (F3.2a): elf Abläufe mit Webserver und Browser gegen Java-Filius (`WebRun`, mit den Dateien des Servers): je Karte Frames, Zeiten, Tabellen, die Fensterzeilen des Servers, Seiten und Bilder des Browsers (Fehlerseiten nach Status); Javas Threads hinken bei langen Übertragungen bis zu 130 ms nach, verglichen auf 200 ms; drei benannte Java-Fehler (`JAVA_BUGS`: `/../` liefert fremde Dateien, `HALLO` beendet die Verbindung stumm, nur ein SYN). `programLines.ts`: die Fensterzeilen der Programme auf Deutsch, geteilt mit `echoJava.test.ts` |
|
||||
| `test/routerWebJava.test.ts`, `test/scenarios/router-web.scenario.json`, `test/scenarios/routerweb-java-*.java.json` | — | neu (F3.2b): sechs Abläufe mit den Seiten der Router und Heimrouter gegen Java-Filius (`RouterWebRun`): Index, Routen (Weiterleitung und RIP), Protokoll nach verworfenem Ping und Regeltreffern, Heimrouter von innen und außen; seit F3.2c drei mit `konfig`: die Seite, „ICMP filtern“ aus und der Ping kommt durch, eine Regel angelegt und nach oben geschoben sperrt den Browser aus (benannter Java-Fehler: nur ein SYN); Startnummern ohne die Million (Javas Router-Server lauscht nicht immer), die Gültigkeit der RIP-Routen als Platzhalter (Javas versetzter Start); ein benannter Java-Fehler (`</titel>`) |
|
||||
@@ -95,6 +95,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
||||
| `test/javaBeansWriter.test.ts`, `test/beans/` | — | neu: 11 Dateien des Schreibers, von Javas `XMLDecoder` gelesen und beschrieben; Erwartungen von Hand |
|
||||
| `src/projectSnapshot.ts` | Swift `TopologyNetworkRuntimeTopologySnapshot(editorState:)` | neu (Abnahme F2): das importierte Projekt als Schnappschuss des Kerns; nur die Kabel verbinden (Swifts Verbindung über die SSID nicht übernommen), nur eingeschaltete DNS-Server laufen; seit F3.1 jeder installierte Echo-Server (ein oder aus, er lässt sich zur Laufzeit schalten) und einfache Client; seit F3.2a jeder Webserver und Browser und die Dateisysteme; seit F3.2c der Rückweg `projectWithSimulationChanges`: die Firewalls aus dem Schnappschuss des Laufs ins Projekt, wie Java sie beim Speichern hat; seit F3.3a die installierten E-Mail-Server (ein oder aus) und auf dem Rückweg ihre Postfächer; seit F3.3b die installierten E-Mail-Programme und auf dem Rückweg ihre Ordner; seit F3.4a die Knoten mit der Befehlszeile (ihre Dateien sind die des Projekts) |
|
||||
| `test/examplesJava.test.ts`, `test/examples/*.java.json` | — | neu (Abnahme F2): fünf Beispiele von Filius (`internetwork_komplett`, `einfaches_rechnernetz_komplett`, `dns_server`, `nat_with_mapped_port`, `Internet_Komplett_…_DE`) mit dem eigenen Leser eingelesen und im Kern gespielt, gegen Java-Filius (`ExampleRun`): je Karte Frames, Zeiten und Tabellenzeilen, die Aufrufe, die Adressen am Ende; gleichzeitig startende DHCP-Clients je Client; zwei benannte Java-Fehler (`JAVA_BUGS`: Ping ohne Gateway wirft eine `NullPointerException`; NAT-Eintrag nur für die erste Gegenstelle) |
|
||||
| `test/classicsJava.test.ts`, `test/exampleRun.ts`, `test/examples/{web,email,gnutella}.java.json` | — | neu (Abnahme F3): die Unterrichtsklassiker auf Beispielen von Filius gegen Java-Filius (`ExampleRun`): Webseite über ihren DNS-Namen (`internetwork_komplett`), E-Mail zwischen zwei Netzen durch NAT und Weitergabe von Server zu Server (`Internet_Komplett_…_DE`), Datei per Gnutella über einen Router finden und holen, Ping, Pong und Query weitergereicht (`zwei_rechnernetze_komplett`, Programme vom Test eingesetzt); je Karte Frames, Zeiten und Tabellenzeilen (ohne RIP), die Zeilen jedes Programms, Mails, Teilnehmer, Ergebnisse und Dateien am Ende; Javas Fehler aus F3.6b und `closedPort` benannt (`JAVA_BUGS`); gleich lange RIP-Wege nach Javas Wahl (`equalRipTies`). `exampleRun.ts`: was beide Beispiel-Tests teilen (Datei öffnen, Javas Namen, Karten nach MAC) |
|
||||
| `src/topologyWriter.ts` | Java `SzenarioVerwaltung.netzwerkSpeichern`, Konstruktoren von `NetzwerkInterface`, `Port`, `GUIKabelItem` und der Systemsoftware | neu nach Java: `konfiguration.xml` aus Javas Sicht — Geräte, Schnittstellen (vom Konstruktor angelegte über Getter, weitere per `add`), Ports über Getter-Verweise, Kabel samt Zeichen-Enden (`kabelpanel`), Doku-Elemente; Systemsoftware über ihre Setter: SSID, Lernzeit, RIP, manuelle Routen, DHCP-Client und -Server, statische NAT, Modem, Router-Firewall (über das `konfig`-Plug-in des Webservers, wie XMLEncoder sie erreicht); eigene Anwendungen eines Projekts, wie die Datei sie hatte; Dateisysteme (an die Wurzel des Konstruktors angehängt wie bei XMLEncoder, beim Router hinter dessen Vorlagen); installierte Programme nach den Dateien (Webserver, Tauschbörse und E-Mail-Server legen ihre Ordner nur an, wo sie fehlen; der DNS-Server liest `/dns/hosts` beim Installieren), je unter ihrem Schlüssel mit der Systemsoftware wie `installApp`, samt Einstellungen und persönlicher Firewall; E-Mail: Domain und Bean-Konten des Servers samt Postfächern, Ordner und Adressbuch des Clients (sein Konto steht in `/konten.txt`, einer Datei); Javas Fehler nicht übernommen: ein Empfänger, den eine ältere Datei als Zeichenkette hält, wird als der `AddressEntry` geschrieben, den Javas `AddressEntry(String)` daraus macht (Java legt die Zeichenkette ungeprüft in die typisierte Liste, sein E-Mail-Fenster scheitert daran), ein `null`-Empfänger fällt weg; Kopf „Filius version: 2.14.0 (mdg-filius)“; seit der Abnahme F2 `ipForwardingEnabled`, wo es vom Standard der Klasse abweicht |
|
||||
| `test/topologyWriter.test.ts`, `test/written.java.json` | — | neu: alle 23 Korpus-Dateien gelesen und neu geschrieben, von Java geladen — Topologie, Systemsoftware, Dateisysteme und Programme samt E-Mail wie beim Original (bis auf die benannte Abweichung bei Empfängern als Zeichenkette, nur in `man-in-the-middle.fls`) (Java gegen Java); dazu eine künstliche Topologie mit dem, was der Korpus nicht hat (u. a. eingeschaltete Router-Firewall mit Regeln jedes Protokolls, alle Firewall-Schalter abseits der Vorgabe, Modem mit eigenem Port und Ziel, verschachtelter Dateibaum mit umbenannter Wurzel, namenlosem Ordner, Datei ohne Typ und Inhalt mit CR/CRLF und Zeichen jenseits der BMP, Heimrouter mit eigener Wurzel, jedes Filius-Programm mit Einstellungen abseits der Vorgabe samt persönlicher Firewall, von Java angelegte Ordner `/peer2peer` und `/mailserver`, E-Mail-Server und -Client mit Konto, Postfach, drei Ordnern, Adressbuch und Empfängern aller Formen) |
|
||||
| `src/exportProject.ts` | Gegenstück zu `importProject.ts`; `saveProjectArchive` schreibt eine ganze `.fls` samt der übrigen Archiveinträge (Gegenstück: `openProjectArchive` in `importProject.ts`, das sie in `archiveEntries` des Modells legt); Java `JHostKonfiguration.updateWifiConnection`, `InternetKnotenBetriebssystem.getStandardGateway`/`setStandardGateway` | neu: Editor-Modell → Javas Sicht für den Schreiber, Schritt X1 Topologie — Geräte, Schnittstellen (MAC wie in der Simulation: aus der Datei oder aus der Port-ID abgeleitet), Kabel samt Funk (Karte drahtlos und Switch, wie Java verbindet), Gateway und DNS an allen Schnittstellen wie Javas eigene Setter, Doku-Elemente; X2 Systemsoftware — SSIDs, Lernzeit, Routen, RIP, DHCP-Client und -Server, Portweiterleitung, Firewall (Namen des Kerns als Javas Zahlen), Modems aus den Paaren des Modells (Port aus dem Paarnamen, der erste eines Paars dient, Ziel `localhost`); X3 Dateien von Rechner und Notebook — Typ aus der Inhaltsart (Text nach Endung, sonst `text`; Bild und Binärdaten nach Medientyp), Bilder und Binärdaten in Base64 wie Filius' `Base64.encodeBytes` (Umbruch nach 76 Zeichen, an 19 Längen mit Java gemessen); X4a Programme — je Programm die Filius-Klasse unter ihrem Namen als Schlüssel, eingeschaltete Server, Ports, virtuelle Hosts, DNS-Rekursion, Ziel des Clients (leer als `null` wie Javas Konstruktor), Teilnehmerzahl, persönliche Firewall; X4b E-Mail — Beans des Servers (Domain, Konten, Postfächer) und des Clients (drei Ordner, Adressbuch), `/konten.txt` und `/mailserver/konten.txt` aus den Konten des Modells neu geschrieben im Format von Filius 2.14 (`EmailKonto.toString`, `listeBenutzerkontenZuString`), ohne Javas Fehler (leere Felder statt "null"); ein Zeichen, das ein Feld im Format nicht tragen kann, wird dort zum Leerzeichen, mit Warnung; seit der Abnahme F2 die IP-Weiterleitung aus dem Modell |
|
||||
|
||||
@@ -2189,6 +2189,20 @@ describe('RIP', () => {
|
||||
expect(engine.sendIPv4Packet(routerAId, packet)).toEqual({ type: 'icmpError', packetIdentity: packet.identity, nodeId: routerAId, kind: 'destinationNetworkUnreachable' });
|
||||
});
|
||||
|
||||
it('keeps the route it learned first when another router tells one as long', () => {
|
||||
// Java keeps whichever its threads handle first (classicsJava.test.ts, equalRipTies); the engine the first it hears.
|
||||
const engine = ripTopology();
|
||||
fromB(engine, '10.0.0.3\n10.0.0.3\n16\n75000\n203.0.113.0 255.255.255.0 1');
|
||||
const expires = route(engine, routerAId, '203.0.113.0')!.expiresAtMilliseconds;
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 5_000 });
|
||||
fromB(engine, '10.0.0.2\n10.0.0.2\n16\n75000\n203.0.113.0 255.255.255.0 1');
|
||||
// Nor does that one keep it alive.
|
||||
expect(route(engine, routerAId, '203.0.113.0')).toMatchObject({ metric: 2, nextHop: '10.0.0.3', expiresAtMilliseconds: expires });
|
||||
// A shorter one replaces it.
|
||||
fromB(engine, '10.0.0.2\n10.0.0.2\n16\n75000\n203.0.113.0 255.255.255.0 0');
|
||||
expect(route(engine, routerAId, '203.0.113.0')).toMatchObject({ metric: 1, nextHop: '10.0.0.2' });
|
||||
});
|
||||
|
||||
it('forwards along the fewest hops, then the longest mask, and not along 16 hops', () => {
|
||||
const engine = ripTopology();
|
||||
// The /24 through a router at 10.0.0.3, which is not there.
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('the Gnutella program in the engine', () => {
|
||||
ping(engine, '5//0x00//0//8//0//192.168.0.20');
|
||||
engine.gnutella(ECHO)!.reset();
|
||||
expect(engine.gnutella(ECHO)!.peers).toEqual([]);
|
||||
expect(engine.gnutella(ECHO)!.log.all.map((line) => line.peers)).toEqual([['192.168.0.20'], []]);
|
||||
expect(engine.gnutella(ECHO)!.log.all.flatMap((line) => (line.kind === 'peers' ? [line.peers] : []))).toEqual([['192.168.0.20'], []]);
|
||||
});
|
||||
|
||||
it('closes a connection whose text has no packet head, and leaves a packet of another kind (Java: its worker throws on the one)', () => {
|
||||
|
||||
@@ -319,7 +319,7 @@ describe('the Gnutella program as Java FILIUS runs it', () => {
|
||||
const expected = connections(
|
||||
(SEARCHING.has(name) ? beforeServerFins(list) : list)
|
||||
.filter((frame) => frame.time < javaUntil)
|
||||
.map((frame) => ({ time: frame.time, raw: javaText(frame), text: masked(`${withClientPortText(javaText(frame))} ${data(frame.payload)}`) }))
|
||||
.map((frame) => ({ time: frame.time, raw: javaText(frame), text: masked(`${withClientPortText(javaText(frame))} ${data(frame.payload ?? undefined)}`) }))
|
||||
.filter(compared),
|
||||
);
|
||||
const engineUntil = UP_TO_FIRST_HIT.has(name) ? firstEngineHit(engine, scenario) : until;
|
||||
|
||||
@@ -207,6 +207,16 @@ export function expectSameCaptures(
|
||||
tables?: JavaTables;
|
||||
/** Placeholders in a table cell, beyond those of tableCell. */
|
||||
cell?: (text: string) => string;
|
||||
/**
|
||||
* A frame's flow, by its text (after `text`). Where frames of different
|
||||
* flows may come in either order (Java's threads, a few ten
|
||||
* milliseconds apart), a card's frames are compared flow by flow, the
|
||||
* flows by their names: each flow's frames in order, at about the same
|
||||
* time, with their rows.
|
||||
*/
|
||||
flow?: ((text: string) => string) | undefined;
|
||||
/** Which frames are compared, by their text (after `text`); all where not given. */
|
||||
compared?: ((text: string) => boolean) | undefined;
|
||||
} = {},
|
||||
): void {
|
||||
const text = options.text ?? ((each: string) => each);
|
||||
@@ -225,14 +235,31 @@ export function expectSameCaptures(
|
||||
const from = (options.from ?? Number.NEGATIVE_INFINITY) - javaStart;
|
||||
const until = (options.until ?? Number.POSITIVE_INFINITY) - javaStart;
|
||||
const normalized = options.normalized ?? ((frames: SeenFrame[], _card: string) => frames);
|
||||
const compared = options.compared ?? (() => true);
|
||||
const window = (frames: SeenFrame[], start: number) =>
|
||||
frames.filter((frame) => frame.time - start >= from && frame.time - start < until).map((frame) => ({ time: frame.time - start, text: frame.text }));
|
||||
frames
|
||||
.filter((frame) => frame.time - start >= from && frame.time - start < until && compared(frame.text))
|
||||
.map((frame) => ({ time: frame.time - start, text: frame.text }));
|
||||
const inWindow = (time: number, start: number) => time - start >= from && time - start < until;
|
||||
// Stable: a flow's frames keep their order.
|
||||
const inFlows = <T>(items: T[], textOf: (item: T) => string): T[] =>
|
||||
options.flow === undefined
|
||||
? items
|
||||
: items
|
||||
.map((item, index) => ({ item, index, flow: options.flow!(textOf(item)) }))
|
||||
.sort((lhs, rhs) => (lhs.flow < rhs.flow ? -1 : lhs.flow > rhs.flow ? 1 : lhs.index - rhs.index))
|
||||
.map(({ item }) => item);
|
||||
for (const [card, javaList] of javaByCard) {
|
||||
if (options.tables !== undefined) {
|
||||
// The rows of the frames in the window, frame by frame.
|
||||
const javaNumbers = javaList.flatMap((frame, index) => (inWindow(frame.time, javaStart) ? [index + 1] : []));
|
||||
const engineFrames = capturedByCard.get(card)!.filter((each) => inWindow(each.timeMilliseconds, engineStart));
|
||||
const javaNumbers = inFlows(
|
||||
javaList.flatMap((frame, index) => (inWindow(frame.time, javaStart) && compared(frame.text) ? [index + 1] : [])),
|
||||
(number) => javaList[number - 1]!.text,
|
||||
);
|
||||
const engineFrames = inFlows(
|
||||
capturedByCard.get(card)!.filter((each) => inWindow(each.timeMilliseconds, engineStart) && compared(text(engineText(each.frame)))),
|
||||
(each) => text(engineText(each.frame)),
|
||||
);
|
||||
for (const [view, withAddresses] of [['rows', true], ['layeredRows', false]] as const) {
|
||||
const javaRows = options.tables[view]?.[card];
|
||||
if (javaRows === undefined) continue;
|
||||
@@ -250,8 +277,8 @@ export function expectSameCaptures(
|
||||
}
|
||||
}
|
||||
}
|
||||
const expected = normalized(window(javaList, javaStart), card);
|
||||
const actual = normalized(window(engineByCard.get(card)!, engineStart), card);
|
||||
const expected = inFlows(normalized(window(javaList, javaStart), card), (frame) => frame.text);
|
||||
const actual = inFlows(normalized(window(engineByCard.get(card)!, engineStart), card), (frame) => frame.text);
|
||||
for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) {
|
||||
expect(actual[index]?.text, `${card} frame #${index + 1}`).toBe(expected[index]?.text);
|
||||
expect(Math.abs(actual[index]!.time - expected[index]!.time), `${card} time of frame #${index + 1}`).toBeLessThanOrEqual(
|
||||
@@ -268,8 +295,8 @@ export function withClientPortText(text: string): string {
|
||||
|
||||
/** Java's capture tables per card: number, source, destination, protocol, layer, comment (ScenarioNetwork.writeRows). */
|
||||
export interface JavaTables {
|
||||
rows?: Record<string, (string | null)[][]>;
|
||||
layeredRows?: Record<string, (string | null)[][]>;
|
||||
rows?: Record<string, (string | null)[][]> | undefined;
|
||||
layeredRows?: Record<string, (string | null)[][]> | undefined;
|
||||
}
|
||||
|
||||
/** Java's layer names and comment words: its German message bundle, the locale it runs in (Locale.GERMANY). */
|
||||
|
||||
@@ -542,6 +542,7 @@ describe('the command line on the network beyond the stories', () => {
|
||||
return [router(number), [...back, ...on]];
|
||||
}),
|
||||
),
|
||||
steps: [],
|
||||
};
|
||||
const engine = watchedEngine(chain, withSnapshot(snapshotOf(chain), { language: 'de', terminalNodeIds: new Set([client]) })).engine;
|
||||
engine.terminal(client)!.enter(`traceroute 10.${ROUTERS + 1}.0.10`);
|
||||
@@ -563,7 +564,7 @@ describe('the command line on the network beyond the stories', () => {
|
||||
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'terminal-net.scenario.json'), 'utf8')) as Scenario;
|
||||
// The first frame comes about 110 ms after 3 s: at a time of day with two digits of milliseconds (Java pads to three).
|
||||
const base = new Date(2026, 9, 2, 10, 37, 6, 900).getTime();
|
||||
const engine = new NetworkRuntimeEngine(scenario.seed, undefined, { wallClock: () => base + engine.state.currentTimeMilliseconds });
|
||||
const engine: NetworkRuntimeEngine = new NetworkRuntimeEngine(scenario.seed, undefined, { wallClock: () => base + engine.state.currentTimeMilliseconds });
|
||||
engine.handle({
|
||||
type: 'start',
|
||||
snapshot: withSnapshot(snapshotOf(scenario), { language: 'de', terminalNodeIds: new Set(HOSTS) }),
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
// The classics of the classroom on FILIUS' own examples, as Java FILIUS runs
|
||||
// them (the acceptance of F3): a web page loaded by its DNS name, an e-mail
|
||||
// sent between two networks, a file found and fetched with Gnutella.
|
||||
// tools/java-oracle's ExampleRun opened each file in Java FILIUS as FILIUS
|
||||
// opens it, started it, called the programs as their windows call them and
|
||||
// wrote every frame each card saw, the capture's tables, what each program
|
||||
// told its window and where the programs ended (examples/<story>.java.json).
|
||||
// Here the file is read with this package's reader and run in the core
|
||||
// (projectSnapshot), the same calls at the same times: each card's frames
|
||||
// in the same order, with the same content and rows, at about the same
|
||||
// time; each program's lines in the same order at about the same time; the
|
||||
// same mails, peers, results and files at the end.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
formatMail,
|
||||
GNUTELLA_FOLDER,
|
||||
javaBase64,
|
||||
javaFileTypeName,
|
||||
NetworkRuntimeEngine,
|
||||
VirtualFileSystem,
|
||||
type Id,
|
||||
type LogLine,
|
||||
type MailboxEntry,
|
||||
} from '@mdg-filius/core';
|
||||
import { projectSnapshot, type ImportedProject } from '../src/index.js';
|
||||
import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, withClientPortText, type JavaFrame, type JavaTables } from '../../core/test/javaRun.js';
|
||||
import { browserText, emailClientText, emailServerText, isPolled, POLL_MILLISECONDS, serverText } from '../../core/test/programLines.js';
|
||||
import { exampleCards, exampleRecording, namesInJavaOrder, openExample } from './exampleRun.js';
|
||||
|
||||
const SEED = 14;
|
||||
|
||||
interface JavaRun extends JavaTables {
|
||||
frames: Record<string, JavaFrame[]>;
|
||||
cards: Record<string, string>;
|
||||
/** What each program told its window: time, program (its node's name, or "browser"), text; a call the story made as "call …". */
|
||||
programs: [number, string, string][];
|
||||
/** Where the programs ended (ExampleRun). */
|
||||
end: Record<string, unknown>;
|
||||
/** Every change of each RIP router's table: time, router, `network/mask gateway interface hops; …`. */
|
||||
ripTables: [number, string, string][];
|
||||
}
|
||||
|
||||
type Lines = Record<string, [number, string][]>;
|
||||
type Node = (name: string) => Id;
|
||||
type JavaMail = [string, string, string];
|
||||
|
||||
interface Classic {
|
||||
readonly file: string;
|
||||
/** Changes to the example before it starts, as ExampleRun makes them. */
|
||||
readonly prepare?: (project: ImportedProject, node: Node) => void;
|
||||
/**
|
||||
* At the first call: RIP routes the engine's routers chose otherwise than
|
||||
* Java's where both ways are as long (equalRipTies).
|
||||
*/
|
||||
readonly ripTies?: true;
|
||||
/** Placeholders in a table cell of the story (the frames' texts carry no data). */
|
||||
readonly cell?: (text: string) => string;
|
||||
/**
|
||||
* Where frames of different connections may come in either order: a
|
||||
* frame's flow (expectSameCaptures), by its text.
|
||||
*/
|
||||
readonly flow?: (text: string) => string;
|
||||
/** Changes to the engine before it runs, from Java's run. */
|
||||
readonly beforeRun?: (engine: NetworkRuntimeEngine, java: JavaRun) => void;
|
||||
/** A call the story made at a program ("call …" in Java's lines, without "call "). */
|
||||
readonly act: (engine: NetworkRuntimeEngine, node: Node, program: string, call: string) => void;
|
||||
/** Java's lines by program, as its windows show them (without the calls). */
|
||||
readonly javaLines: (java: JavaRun) => Lines;
|
||||
readonly engineLines: (engine: NetworkRuntimeEngine, node: Node) => Lines;
|
||||
/** Where the engine's programs ended, as ExampleRun writes Java's. */
|
||||
readonly end: (engine: NetworkRuntimeEngine, node: Node) => unknown;
|
||||
/**
|
||||
* The waits Java ends only at a look every 100 ms, one after the other, on
|
||||
* the way to a line (a connection opened); a line is compared within
|
||||
* GAP_TOLERANCE_MILLISECONDS and these looks.
|
||||
*/
|
||||
readonly looks: number;
|
||||
/**
|
||||
* When the DHCP clients are done: the cards are compared from the first
|
||||
* call on (the leases before it are the acceptance of F2, examplesJava's
|
||||
* firewall story).
|
||||
*/
|
||||
readonly afterLeases?: true;
|
||||
}
|
||||
|
||||
function lines<E>(log: readonly LogLine<E>[] | undefined, text: (entry: E) => string | undefined): [number, string][] {
|
||||
return (log ?? []).flatMap((line) => {
|
||||
const each = text(line);
|
||||
return each === undefined ? [] : [[line.timeMilliseconds, withClientPortText(each)] as [number, string]];
|
||||
});
|
||||
}
|
||||
|
||||
/** Java's lines of the programs: what their windows show (no empty notices, null, nor the browser's "update" at its start). */
|
||||
function javaLinesOf(java: JavaRun, programs: readonly string[]): Lines {
|
||||
const told: Lines = Object.fromEntries(programs.map((program) => [program, []]));
|
||||
const firstCall = Math.min(...java.programs.filter(([, , text]) => text.startsWith('call ')).map(([time]) => time));
|
||||
for (const [time, program, text] of java.programs) {
|
||||
if (text === 'null' || text.startsWith('call ') || (time < firstCall && text === 'update')) continue;
|
||||
told[program]!.push([time, withClientPortText(text)]);
|
||||
}
|
||||
return told;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a router has two ways to a network as long as each other, Java takes
|
||||
* the one whose message its threads handle first (examplesJava's firewall
|
||||
* story; tools/java-oracle/README.md, Abnahme F2), the engine the first it
|
||||
* learned; neither replaces it with a way as long. As a random number Java
|
||||
* drew, Java's choice is given to the engine (its table at the time),
|
||||
* route by route; a route that differs otherwise (another length, or one
|
||||
* of them missing) is a difference, not a tie.
|
||||
*/
|
||||
function equalRipTies(engine: NetworkRuntimeEngine, java: JavaRun, node: Node, time: number): void {
|
||||
const latest = new Map<string, string>();
|
||||
for (const [changed, router, table] of java.ripTables) if (changed <= time) latest.set(router, table);
|
||||
for (const [router, table] of latest) {
|
||||
const routes = [...(engine.state.ripTablesByNodeId.get(node(router)) ?? [])];
|
||||
for (const entry of table.split('; ')) {
|
||||
const [destination, gateway, card, hops] = entry.split(' ') as [string, string, string, string];
|
||||
const index = routes.findIndex((route) => `${route.destinationNetwork}/${route.subnetMask}` === destination);
|
||||
const route = routes[index];
|
||||
expect(route?.metric, `${router}: ${destination}`).toBe(Number(hops));
|
||||
if (route!.nextHop === gateway) continue;
|
||||
// Java's hopPublicIp: the first address of the router that told it, as its other routes through that router say.
|
||||
const told = routes.find((each) => each.nextHop === gateway && each.hopPublicIPAddress !== undefined)?.hopPublicIPAddress;
|
||||
routes[index] = { ...route!, nextHop: gateway, interfaceIPAddress: card, ...(told === undefined ? {} : { hopPublicIPAddress: told }) };
|
||||
}
|
||||
engine.state.ripTablesByNodeId.set(node(router), routes);
|
||||
}
|
||||
}
|
||||
|
||||
const mails = (entries: readonly MailboxEntry[]): JavaMail[] => entries.map((entry) => [formatMail(entry.mail), String(entry.isNew), String(entry.isMarkedForDeletion)]);
|
||||
|
||||
const GMX = 'mail.gmx.de (130.100.200.25)';
|
||||
const WEBMAIL = 'mail.webmail.de (140.100.200.25)';
|
||||
const GNUTELLA_PEERS = ['Notebook 1.10', 'Notebook 1.11', 'DNS-Server 2.10'];
|
||||
|
||||
const CLASSICS: Record<string, Classic> = {
|
||||
web: {
|
||||
file: 'internetwork_komplett.fls',
|
||||
act: (engine, node, _program, call) => engine.webBrowser(node('Notebook 1.10'))!.open(call.slice('holeWebseite '.length)),
|
||||
javaLines: (java) => javaLinesOf(java, ['browser', 'Webserver 5.10']),
|
||||
engineLines: (engine, node) => ({
|
||||
browser: lines(engine.webBrowser(node('Notebook 1.10'))!.log.all, browserText),
|
||||
'Webserver 5.10': lines(engine.webServer(node('Webserver 5.10'))!.log.all, serverText),
|
||||
}),
|
||||
end: () => ({}),
|
||||
looks: 1,
|
||||
},
|
||||
email: {
|
||||
file: 'Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE.fls',
|
||||
act: (engine, node, program, call) => {
|
||||
const client = engine.emailClient(node(program))!;
|
||||
if (call === 'emailsAbholenEmails') {
|
||||
expect(client.fetch()).toBe(true);
|
||||
} else {
|
||||
expect(client.send({ to: call.slice('versendeEmail '.length), cc: '', bcc: '', subject: 'Treffen', text: 'Hallo Frimp,\nbis morgen!\nHerbert' })).toBeUndefined();
|
||||
}
|
||||
},
|
||||
javaLines: (java) => javaLinesOf(java, ['Notebook', 'Notebook#2', GMX, WEBMAIL]),
|
||||
engineLines: (engine, node) => ({
|
||||
Notebook: lines(engine.emailClient(node('Notebook'))!.log.all, emailClientText),
|
||||
'Notebook#2': lines(engine.emailClient(node('Notebook#2'))!.log.all, emailClientText),
|
||||
[GMX]: lines(engine.emailServer(node(GMX))!.log.all, (entry) => emailServerText(entry)),
|
||||
[WEBMAIL]: lines(engine.emailServer(node(WEBMAIL))!.log.all, (entry) => emailServerText(entry)),
|
||||
}),
|
||||
end: (engine, node) => ({
|
||||
[GMX]: engine.emailServer(node(GMX))!.state.accounts.map((each) => [each.username, mails(each.mailbox)]),
|
||||
[WEBMAIL]: engine.emailServer(node(WEBMAIL))!.state.accounts.map((each) => [each.username, mails(each.mailbox)]),
|
||||
...Object.fromEntries(
|
||||
['Notebook', 'Notebook#2'].map((name) => {
|
||||
const client = engine.emailClient(node(name))!;
|
||||
return [name, { inbox: mails(client.inbox), sent: mails(client.sent) }];
|
||||
}),
|
||||
),
|
||||
}),
|
||||
// Two connections one after the other, each opened at a look: herbert's to mail.gmx.de, then the
|
||||
// server's on to mail.webmail.de (measured: Java some 80 ms later at each; the fetch is a call of its own).
|
||||
looks: 2,
|
||||
afterLeases: true,
|
||||
// #3 reaches the school's network (42.0.0.0) through "" or #2, two hops each: every answer to the school.
|
||||
ripTies: true,
|
||||
},
|
||||
gnutella: {
|
||||
file: 'zwei_rechnernetze_komplett.fls',
|
||||
prepare: (project, node) => {
|
||||
for (const name of GNUTELLA_PEERS) {
|
||||
project.installedPrograms.get(node(name))!.add('gnutella');
|
||||
project.gnutellaConfigurations.set(node(name), { maximumKnownPeers: 3 });
|
||||
}
|
||||
const files = project.virtualFileSystems.get(node('DNS-Server 2.10')) ?? new VirtualFileSystem();
|
||||
project.virtualFileSystems.set(node('DNS-Server 2.10'), files);
|
||||
files.createDirectory(GNUTELLA_FOLDER, true);
|
||||
files.writeTextFile(`${GNUTELLA_FOLDER}/Lied der Woche.txt`, 'La la la');
|
||||
},
|
||||
// A hit's size and a pong's bytes, with the payload length that counts them (JAVA_BUGS; gnutellaJava.test.ts).
|
||||
cell: (text) => text.replace(/: \d+ B\/\//g, ': n B//').replace(/(\/\/0x01\/\/\d+\/\/\d+\/\/)\d+(\/\/.*\/\/6346\/\/\d+\/\/)\d+$/, '$1n$2n'),
|
||||
// A search goes to both peers of 1.10 at once, and at its card the frames of the two connections come in
|
||||
// either order (gnutellaJava.test.ts, BY_CONNECTION): compared direction by direction between two hosts on
|
||||
// the server's port (the query's and the download's connection to 2.10 one after the other), ARP on its own.
|
||||
flow: (text) => / TCP (\S+) > (\S+) /.exec(text)?.slice(1).join(' > ') ?? 'ARP',
|
||||
// The ids of Java's own pings and queries (a ping or query with no hops yet), in the order they were made:
|
||||
// the engine draws its own, these take their place (gnutellaJava.test.ts).
|
||||
beforeRun: (engine, java) => {
|
||||
const own = Object.values(java.frames)
|
||||
.flat()
|
||||
.flatMap((frame) => {
|
||||
const match = /^(\d+)\/\/0x(?:00|80)\/\/0\/\//.exec(frame.payload ?? '');
|
||||
return match === null ? [] : [[frame.time, Number(match[1])] as const];
|
||||
})
|
||||
.sort((lhs, rhs) => lhs[0] - rhs[0]);
|
||||
const ids = [...new Set(own.map(([, guid]) => guid))];
|
||||
engine.drawGnutellaGuid = () => ids.shift() ?? 1;
|
||||
},
|
||||
act: (engine, node, program, call) => {
|
||||
const peer = engine.gnutella(node(program))!;
|
||||
if (call.startsWith('join ')) peer.join(call.slice('join '.length));
|
||||
else if (call.startsWith('search ')) peer.search(call.slice('search '.length));
|
||||
else peer.download(Number(call.slice('download '.length)));
|
||||
},
|
||||
// Each program's peers and results as it told them, each change once (Java tells its window also when
|
||||
// nothing changed, and empty lists at its start); results only when there are some.
|
||||
javaLines: (java) => {
|
||||
const told: Lines = Object.fromEntries(GNUTELLA_PEERS.flatMap((name) => [[`${name} peers`, []], [`${name} results`, []]]));
|
||||
for (const [time, program, text] of java.programs) {
|
||||
const [kind, list] = [text.slice(0, text.indexOf(' ')), text.slice(text.indexOf(' ') + 1)];
|
||||
if (kind === 'call' || (kind === 'results' && list === '')) continue;
|
||||
const each = told[`${program} ${kind}`]!;
|
||||
if ((each.at(-1)?.[1] ?? '') !== list) each.push([time, list]);
|
||||
}
|
||||
return told;
|
||||
},
|
||||
engineLines: (engine, node) =>
|
||||
Object.fromEntries(
|
||||
GNUTELLA_PEERS.flatMap((name) =>
|
||||
(['peers', 'results'] as const).map((wanted) => {
|
||||
const told: [number, string][] = [];
|
||||
for (const line of engine.gnutella(node(name))!.log.all) {
|
||||
if (line.kind !== wanted) continue;
|
||||
const text = line.kind === 'peers' ? line.peers.join(',') : line.results.join('|');
|
||||
if ((wanted === 'results' && text === '') || (told.at(-1)?.[1] ?? '') === text) continue;
|
||||
told.push([line.timeMilliseconds, text]);
|
||||
}
|
||||
return [`${name} ${wanted}`, told];
|
||||
}),
|
||||
),
|
||||
),
|
||||
end: (engine, node) =>
|
||||
Object.fromEntries(
|
||||
GNUTELLA_PEERS.map((name) => {
|
||||
const peer = engine.gnutella(node(name))!;
|
||||
const files = engine.state.topologySnapshot.fileSystemsByNodeId.get(node(name))!;
|
||||
const shared = files.contains(GNUTELLA_FOLDER)
|
||||
? files.entriesInCreationOrder(GNUTELLA_FOLDER).flatMap((entry): [string, string, string][] => {
|
||||
const content = entry.content;
|
||||
if (content.kind === 'directory') return [];
|
||||
const fileName = entry.path.slice(entry.path.lastIndexOf('/') + 1);
|
||||
return [[fileName, javaFileTypeName(fileName, content), content.kind === 'text' ? content.text : javaBase64(content.data)]];
|
||||
})
|
||||
: [];
|
||||
return [name, { peers: [...peer.peers], results: [...peer.results], files: shared }];
|
||||
}),
|
||||
),
|
||||
looks: 2,
|
||||
},
|
||||
};
|
||||
|
||||
/** A hit's size as Java's Datei.holeGroesse says it (the text decoded as Base64), and as the engine (its characters; gnutellaJava.test.ts). */
|
||||
const withHitSize = (text: string) => text.replace('/Lied der Woche.txt: 3 B', '/Lied der Woche.txt: 8 B');
|
||||
|
||||
/** Java's run without the frames `keep` refuses, each card's table rows numbered anew. */
|
||||
function withFrames(java: JavaRun, keep: (frame: JavaFrame, card: string, frames: readonly JavaFrame[]) => boolean): JavaRun {
|
||||
const numbers = new Map<string, Map<number, number>>();
|
||||
const frames = Object.fromEntries(
|
||||
Object.entries(java.frames).map(([card, list]) => {
|
||||
const renumbered = new Map<number, number>();
|
||||
const kept = list.filter((frame, index) => {
|
||||
if (!keep(frame, card, list)) return false;
|
||||
renumbered.set(index + 1, renumbered.size + 1);
|
||||
return true;
|
||||
});
|
||||
numbers.set(card, renumbered);
|
||||
return [card, kept];
|
||||
}),
|
||||
);
|
||||
const tables = (rows: Record<string, (string | null)[][]> | undefined) =>
|
||||
rows === undefined
|
||||
? undefined
|
||||
: Object.fromEntries(
|
||||
Object.entries(rows).map(([card, list]) => [
|
||||
card,
|
||||
list.flatMap((row) => {
|
||||
const number = numbers.get(card)?.get(Number(row[0]));
|
||||
return number === undefined ? [] : [[String(number), ...row.slice(1)]];
|
||||
}),
|
||||
]),
|
||||
);
|
||||
return { ...java, frames, rows: tables(java.rows), layeredRows: tables(java.layeredRows) };
|
||||
}
|
||||
|
||||
/** A Gnutella connection by its client's end. */
|
||||
const clientEnd = (frame: JavaFrame) => (frame.sourcePort === 6346 ? `${frame.destinationIp}:${frame.destinationPort}` : `${frame.sourceIp}:${frame.sourcePort}`);
|
||||
|
||||
/**
|
||||
* Where Java errs, per story: its run corrected, as Java means it.
|
||||
*
|
||||
* gnutella: a hit names the file's size in characters, where Java decodes
|
||||
* its text as Base64 first (gnutella.ts); in the frames' rows the sizes
|
||||
* are masked (Classic.cell), as a pong's bytes are. And Java's server closes
|
||||
* a connection after its hits; the engine keeps it, as it keeps every other
|
||||
* (gnutella.ts): each connection whose server sends the first FIN, without
|
||||
* its frames from that FIN on (gnutellaJava.test.ts, beforeServerFins).
|
||||
* And 2.10 passes the query on to 1.11, which has no gateway and cannot
|
||||
* answer: Java's connectClientMode sends the SYN once and gives up after one
|
||||
* wait (tcpJava.test.ts, closedPort); the engine sends it three times, a
|
||||
* wait of three round trips each. The two later SYNs added on every card
|
||||
* the first showed on (without rows of Java's).
|
||||
*/
|
||||
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
|
||||
const toUnreachable = (frame: JavaFrame) => frame.tcpFlags === 'S' && frame.destinationIp === '141.99.1.11' && frame.sourceIp === '141.99.2.10';
|
||||
|
||||
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
gnutella: (java) =>
|
||||
withSynRetries(
|
||||
withFrames(
|
||||
{
|
||||
...java,
|
||||
programs: java.programs.map(([time, program, text]) => [time, program, withHitSize(text)]),
|
||||
end: JSON.parse(withHitSize(JSON.stringify(java.end))) as Record<string, unknown>,
|
||||
},
|
||||
(frame, _card, frames) => {
|
||||
if (frame.tcpFlags === undefined) return true;
|
||||
const firstFin = frames.find((each) => each.tcpFlags?.includes('F') === true && clientEnd(each) === clientEnd(frame));
|
||||
return firstFin === undefined || firstFin.sourcePort !== 6346 || frames.indexOf(frame) < frames.indexOf(firstFin);
|
||||
},
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
/** Java's run with the SYN to 1.11 sent twice more, a timeout apart, on every card it showed on. */
|
||||
function withSynRetries(java: JavaRun): JavaRun {
|
||||
return {
|
||||
...java,
|
||||
frames: Object.fromEntries(
|
||||
Object.entries(java.frames).map(([card, list]) => {
|
||||
const syn = list.find(toUnreachable);
|
||||
return [card, syn === undefined ? list : [...list, ...[1, 2].map((attempt) => ({ ...syn, time: syn.time + attempt * JAVA_TIMEOUT_MILLISECONDS }))]];
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial sequence numbers below the millions. Java gives the next number to
|
||||
* each TCP socket as it is made (the core does too), and a server makes its
|
||||
* first listening socket in its own thread when the simulation starts
|
||||
* (ServerAnwendung.annehmenVerbindungen): which server of an example draws
|
||||
* which number is a race of Java's threads.
|
||||
*/
|
||||
const withoutInitialNumbers = (text: string) =>
|
||||
text
|
||||
.replace(/(seq|ack)=(\d+)/g, (_, name: string, value: string) => `${name}=${Number(value) % 1_000_000}`)
|
||||
.replace(/(SEQ|ACK): \d+\.(\d{3}\.\d{3})/g, '$1: n.$2');
|
||||
|
||||
/** A frame's text with placeholders: free ports, DNS message IDs (drawn at random in Java, from the seed in the engine), initial sequence numbers. */
|
||||
const frameText = (text: string) => withoutInitialNumbers(withClientPortText(text)).replace(/ID=\d+/g, 'ID=x');
|
||||
|
||||
/** The example read, changed as the story changes it, and started in the engine. */
|
||||
function started(classic: Classic, java: JavaRun) {
|
||||
const project = openExample(classic.file);
|
||||
const names = namesInJavaOrder(project);
|
||||
const node: Node = (nodeName) => names.get(nodeName)!;
|
||||
classic.prepare?.(project, node);
|
||||
const engine = new NetworkRuntimeEngine(SEED);
|
||||
engine.handle({ type: 'start', snapshot: projectSnapshot(project), seed: SEED, initialTimeMilliseconds: 0 });
|
||||
classic.beforeRun?.(engine, java);
|
||||
return { project, names, node, engine };
|
||||
}
|
||||
|
||||
/** The story played in the engine as Java played it, and compared with Java's run (corrected, JAVA_BUGS). */
|
||||
function expectSameAsJava(name: string, java: JavaRun): void {
|
||||
const classic = CLASSICS[name]!;
|
||||
const { project, names, node, engine } = started(classic, java);
|
||||
|
||||
const calls = java.programs.filter(([, , text]) => text.startsWith('call '));
|
||||
for (const [index, [time, program, text]] of calls.entries()) {
|
||||
engine.handle({ type: 'advance', toMilliseconds: time });
|
||||
if (index === 0 && classic.ripTies !== undefined) equalRipTies(engine, java, node, time);
|
||||
classic.act(engine, node, program, text.slice('call '.length));
|
||||
}
|
||||
const last = Math.max(...Object.values(java.frames).flat().map((frame) => frame.time), ...java.programs.map(([time]) => time));
|
||||
engine.handle({ type: 'advance', toMilliseconds: last + 5_000 });
|
||||
|
||||
const tolerance = GAP_TOLERANCE_MILLISECONDS + classic.looks * POLL_MILLISECONDS;
|
||||
const { cards, withEngineMacs } = exampleCards(project, names, java.cards);
|
||||
const firstCall = calls[0]![0];
|
||||
expectSameCaptures(engine, cards, java.frames, {
|
||||
text: (text) => frameText(withEngineMacs(text)),
|
||||
cell: (text) => (classic.cell ?? ((each: string) => each))(withoutInitialNumbers(withEngineMacs(text))),
|
||||
tables: java,
|
||||
tolerance,
|
||||
// Both run on the same clock: the calls came at the same times.
|
||||
starts: { java: 0, engine: 0 },
|
||||
from: classic.afterLeases === undefined ? undefined : firstCall,
|
||||
flow: classic.flow,
|
||||
// RIP's beacons come every 30 s after random pauses, as many as a router drew since its start, which
|
||||
// depends on the order Java's threads start in (tools/java-oracle/README.md, RIP): RIP is the
|
||||
// acceptance of F2 (examplesJava.test.ts, with its tables), the programs' frames are compared here.
|
||||
compared: (text) => !/ UDP \S+:521 > \S+:520 /.test(text),
|
||||
});
|
||||
|
||||
const expected = classic.javaLines(java);
|
||||
const actual = classic.engineLines(engine, node);
|
||||
expect(Object.keys(actual).sort()).toEqual(Object.keys(expected).sort());
|
||||
for (const [program, expectedLines] of Object.entries(expected)) {
|
||||
const actualLines = actual[program]!;
|
||||
expect(
|
||||
actualLines.map(([, text]) => text),
|
||||
`${program}'s lines`,
|
||||
).toEqual(expectedLines.map(([, text]) => text));
|
||||
for (const [index, [javaTime, text]] of expectedLines.entries()) {
|
||||
const late = javaTime - actualLines[index]![0];
|
||||
const most = (isPolled(text) ? POLL_MILLISECONDS : 0) + tolerance;
|
||||
expect(Math.abs(late), `${program}'s line ${JSON.stringify(text.slice(0, 60))}: Java ${late} ms later`).toBeLessThanOrEqual(most);
|
||||
}
|
||||
}
|
||||
expect(classic.end(engine, node)).toEqual(java.end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Java's run with two frames of one card in each other's place, their rows
|
||||
* too: the order Java's threads give two connections a few ten
|
||||
* milliseconds apart.
|
||||
*/
|
||||
function withSwapped(java: JavaRun, card: string, index: number): JavaRun {
|
||||
const frames = [...java.frames[card]!];
|
||||
[frames[index], frames[index + 1]] = [frames[index + 1]!, frames[index]!];
|
||||
const [first, second] = [String(index + 1), String(index + 2)];
|
||||
const rows = (tables: Record<string, (string | null)[][]> | undefined) =>
|
||||
tables === undefined
|
||||
? undefined
|
||||
: { ...tables, [card]: tables[card]!.map((row) => (row[0] === first ? [second, ...row.slice(1)] : row[0] === second ? [first, ...row.slice(1)] : row)) };
|
||||
return { ...java, frames: { ...java.frames, [card]: frames }, rows: rows(java.rows), layeredRows: rows(java.layeredRows) };
|
||||
}
|
||||
|
||||
describe("the classics of the classroom on FILIUS' examples as Java FILIUS runs them", () => {
|
||||
it('corrects Java only where it errs', () => {
|
||||
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
|
||||
const java = exampleRecording<JavaRun>(name);
|
||||
expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java));
|
||||
}
|
||||
});
|
||||
|
||||
it("gives the engine Java's way only where both are as long", () => {
|
||||
const java = exampleRecording<JavaRun>('email');
|
||||
const firstCall = java.programs.find(([, , text]) => text.startsWith('call '))![0];
|
||||
// #3's route to the school's network, two hops through "" (12.0.0.1) or #2 (3.0.0.1), as Java's table says it.
|
||||
const withRoute = (route: string): JavaRun => ({
|
||||
...java,
|
||||
ripTables: java.ripTables.map(([time, router, table]) => [time, router, router === '#3' ? table.replace(/42\.0\.0\.0\/\S+ \S+ \S+ \d+/, route) : table]),
|
||||
});
|
||||
const given = (recorded: JavaRun) => {
|
||||
const { engine, node } = started(CLASSICS.email!, recorded);
|
||||
engine.handle({ type: 'advance', toMilliseconds: firstCall });
|
||||
equalRipTies(engine, recorded, node, firstCall);
|
||||
return engine.state.ripTablesByNodeId.get(node('#3'))!.find((route) => route.destinationNetwork === '42.0.0.0')!;
|
||||
};
|
||||
for (const [gateway, card] of [['12.0.0.1', '12.0.0.2'], ['3.0.0.1', '3.0.0.2']] as const) {
|
||||
expect(given(withRoute(`42.0.0.0/255.255.255.0 ${gateway} ${card} 2`)), gateway).toMatchObject({ nextHop: gateway, interfaceIPAddress: card, metric: 2 });
|
||||
}
|
||||
// Another length is no tie.
|
||||
expect(() => given(withRoute('42.0.0.0/255.255.255.0 3.0.0.1 3.0.0.2 3'))).toThrow();
|
||||
});
|
||||
|
||||
for (const name of Object.keys(CLASSICS)) {
|
||||
it(`${name}: ${CLASSICS[name]!.file}`, () => {
|
||||
const measured = exampleRecording<JavaRun>(name);
|
||||
expectSameAsJava(name, JAVA_BUGS[name]?.(measured) ?? measured);
|
||||
});
|
||||
}
|
||||
|
||||
it("gnutella: the search's two connections in either order", () => {
|
||||
// A recording before had them the other way round at 1.10's card (CLASSICS.gnutella.flow).
|
||||
const java = JAVA_BUGS.gnutella!(exampleRecording<JavaRun>('gnutella'));
|
||||
const card = Object.entries(java.cards).find(([, each]) => each === 'Notebook 1.10/0')![0];
|
||||
// The first two frames after the search between other hosts each, within 60 ms of each other.
|
||||
const hosts = (frame: JavaFrame) => [frame.sourceIp, frame.destinationIp].sort().join(' ');
|
||||
const frames = java.frames[card]!;
|
||||
const index = frames.findIndex((frame, at) => {
|
||||
const next = frames[at + 1];
|
||||
return frame.time >= 5_500 && frame.tcpFlags !== undefined && next?.tcpFlags !== undefined && hosts(frame) !== hosts(next) && next.time - frame.time <= 60;
|
||||
});
|
||||
expect(index).toBeGreaterThan(0);
|
||||
expectSameAsJava('gnutella', withSwapped(java, card, index));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
// What the tests of FILIUS' own examples share (examplesJava.test.ts, the
|
||||
// acceptance of F2; classicsJava.test.ts, the acceptance of F3): an example
|
||||
// read with this package's reader, its nodes by the names Java gives them,
|
||||
// and each card Java recorded by its MAC address.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { stableMacAddress, type Id } from '@mdg-filius/core';
|
||||
import { openProjectArchive, type ImportedProject } from '../src/index.js';
|
||||
import type { CaptureCard } from '../../core/test/javaRun.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const examples = join(here, '../../../corpus/java-examples');
|
||||
|
||||
/** The recording of a story (examples/<story>.java.json). */
|
||||
export function exampleRecording<T>(story: string): T {
|
||||
return JSON.parse(readFileSync(join(here, 'examples', `${story}.java.json`), 'utf8')) as T;
|
||||
}
|
||||
|
||||
/** An example as this package reads it, its ids numbered as the file's entries come. */
|
||||
export function openExample(file: string): ImportedProject {
|
||||
let counter = 0;
|
||||
return openProjectArchive(new Uint8Array(readFileSync(join(examples, file))), () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||
}
|
||||
|
||||
/** Java's names as ScenarioNetwork.fromFile gives them: a repeated name gets #2, #3, … in the file's order. */
|
||||
export function namesInJavaOrder(project: ImportedProject): Map<string, Id> {
|
||||
const names = new Map<string, Id>();
|
||||
for (const node of project.graph.nodes) {
|
||||
let name = node.displayName;
|
||||
for (let count = 2; names.has(name); count += 1) name = `${node.displayName}#${count}`;
|
||||
names.set(name, node.id);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Each card Java recorded, by its node and index (`name/index`). A card the
|
||||
* file has no MAC address for gets one Java rolls at each load, and the
|
||||
* model one derived from its port: Java's stands for the engine's in the
|
||||
* texts (withEngineMacs).
|
||||
*/
|
||||
export function exampleCards(
|
||||
project: ImportedProject,
|
||||
names: ReadonlyMap<string, Id>,
|
||||
javaCards: Record<string, string>,
|
||||
): { readonly cards: CaptureCard[]; readonly withEngineMacs: (text: string) => string } {
|
||||
const engineMacByJavaMac = new Map<string, string>();
|
||||
const cards: CaptureCard[] = Object.entries(javaCards).map(([mac, card]) => {
|
||||
const [nodeName, index] = card.split('/') as [string, string];
|
||||
const node = project.graph.nodes.find((each) => each.id === names.get(nodeName))!;
|
||||
const port = node.ports[Number(index)]!;
|
||||
engineMacByJavaMac.set(mac.toUpperCase(), (port.importedMacAddress ?? stableMacAddress(port.id)).toUpperCase());
|
||||
return { key: mac, portId: port.id };
|
||||
});
|
||||
const withEngineMacs = (text: string) => text.replace(/[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}/g, (mac) => engineMacByJavaMac.get(mac.toUpperCase()) ?? mac);
|
||||
return { cards, withEngineMacs };
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -8,12 +8,10 @@
|
||||
// table rows (javaRun.ts, cards by their MAC address from the file), the
|
||||
// calls must return the same, the hosts must end with the same addresses.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { interfaceKey, NetworkRuntimeEngine, stableMacAddress, type DnsAddressResolution, type Id, type IPv4DeliveryResult } from '@mdg-filius/core';
|
||||
import { openProjectArchive, projectSnapshot, type ImportedProject } from '../src/index.js';
|
||||
import { projectSnapshot } from '../src/index.js';
|
||||
import { exampleCards, exampleRecording, namesInJavaOrder, openExample } from './exampleRun.js';
|
||||
import {
|
||||
engineText,
|
||||
expectSameCaptures,
|
||||
@@ -21,14 +19,11 @@ import {
|
||||
gaps,
|
||||
javaFrames,
|
||||
withClientPortText,
|
||||
type CaptureCard,
|
||||
type JavaFrame,
|
||||
type JavaTables,
|
||||
type SeenFrame,
|
||||
} from '../../core/test/javaRun.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const examples = join(here, '../../../corpus/java-examples');
|
||||
const SEED = 14;
|
||||
|
||||
interface JavaRun extends JavaTables {
|
||||
@@ -160,31 +155,19 @@ const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Java's names as ScenarioNetwork.fromFile gives them: a repeated name gets #2, #3, … in the file's order. */
|
||||
function namesInJavaOrder(project: ImportedProject): Map<string, Id> {
|
||||
const names = new Map<string, Id>();
|
||||
for (const node of project.graph.nodes) {
|
||||
let name = node.displayName;
|
||||
for (let count = 2; names.has(name); count += 1) name = `${node.displayName}#${count}`;
|
||||
names.set(name, node.id);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
describe("FILIUS' examples as Java FILIUS runs them", () => {
|
||||
it('corrects Java only where it errs', () => {
|
||||
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
|
||||
const java = JSON.parse(readFileSync(join(here, 'examples', `${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const java = exampleRecording<JavaRun>(name);
|
||||
expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java));
|
||||
}
|
||||
});
|
||||
|
||||
for (const [name, story] of Object.entries(STORIES)) {
|
||||
it(`${name}: ${story.file}`, () => {
|
||||
const measured = JSON.parse(readFileSync(join(here, 'examples', `${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const measured = exampleRecording<JavaRun>(name);
|
||||
const java = JAVA_BUGS[name]?.(measured) ?? measured;
|
||||
let counter = 0;
|
||||
const project = openProjectArchive(new Uint8Array(readFileSync(join(examples, story.file))), () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||
const project = openExample(story.file);
|
||||
const names = namesInJavaOrder(project);
|
||||
const engine = new NetworkRuntimeEngine(SEED);
|
||||
engine.handle({ type: 'start', snapshot: projectSnapshot(project), seed: SEED, initialTimeMilliseconds: 0 });
|
||||
@@ -199,17 +182,7 @@ describe("FILIUS' examples as Java FILIUS runs them", () => {
|
||||
engine.handle({ type: 'advance', toMilliseconds: Math.max(engine.state.currentTimeMilliseconds, last) + 1_000 });
|
||||
|
||||
expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => event));
|
||||
// Each card by its node and index. A card the file has no MAC address for gets one Java rolls at
|
||||
// each load, and the model one derived from its port: Java's stands for the engine's in the texts.
|
||||
const engineMacByJavaMac = new Map<string, string>();
|
||||
const cards: CaptureCard[] = Object.entries(java.cards).map(([mac, card]) => {
|
||||
const [nodeName, index] = card.split('/') as [string, string];
|
||||
const node = project.graph.nodes.find((each) => each.id === names.get(nodeName))!;
|
||||
const port = node.ports[Number(index)]!;
|
||||
engineMacByJavaMac.set(mac.toUpperCase(), (port.importedMacAddress ?? stableMacAddress(port.id)).toUpperCase());
|
||||
return { key: mac, portId: port.id };
|
||||
});
|
||||
const withEngineMacs = (text: string) => text.replace(/[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}/g, (mac) => engineMacByJavaMac.get(mac.toUpperCase()) ?? mac);
|
||||
const { cards, withEngineMacs } = exampleCards(project, names, java.cards);
|
||||
const javaStoryStart = java.events[0]?.[0];
|
||||
if (story.storyStart !== undefined) {
|
||||
expectSameCaptures(engine, cards, java.frames, {
|
||||
|
||||
@@ -93,3 +93,5 @@ packages/core/test/filePrograms.test.ts::fileexplorer
|
||||
# "Hallo Welt": the content of the measured example file, quoted where the Gnutella program names Java's size of it.
|
||||
packages/core/src/runtime/apps/gnutella.ts::hallo
|
||||
packages/core/src/runtime/apps/gnutella.ts::welt
|
||||
# GMX: the mail provider of FILIUS' example Internet_Komplett_…_DE (mail.gmx.de, herbert@gmx.de), a proper name.
|
||||
packages/fls/test/classicsJava.test.ts::gmx
|
||||
|
||||
@@ -996,3 +996,69 @@ Weitere Abläufe von `TerminalRun` auf `terminal-net`. Gemessen (02.10.2026):
|
||||
Tabelle (Uhrzeit, Protokoll, Quelle > Ziel, Kommentar), alle 100 ms;
|
||||
Zeilenumbrüche werden zu zwei Leerzeichen; Strg+C beendet es nach dem
|
||||
nächsten Blick. Ein während dessen getippter Befehl wartet bis danach.
|
||||
|
||||
## Unterrichtsklassiker (Abnahme F3)
|
||||
|
||||
`ExampleRun` hat drei weitere Abläufe auf Beispielen von Filius. Was jedes
|
||||
Programm seinem Fenster meldet, steht unter `programs` (Zeit, Programm,
|
||||
Text; ein Aufruf des Ablaufs als `call …`), wo die Programme enden unter
|
||||
`end`:
|
||||
|
||||
* `web` (internetwork_komplett): Notebook 1.10 tippt `www.filius.de` in
|
||||
seinen Browser. Der Name wird bei DNS-Server 2.10 im anderen Netz
|
||||
erfragt, die Seite samt Bild kommt von Webserver 5.10 zwei Router weiter.
|
||||
* `email` (Internet_Komplett_…_DE): Wenn die DHCP-Clients ihre Adressen
|
||||
haben (8 s), schreibt herbert@gmx.de (das erste Notebook, im Schulnetz
|
||||
hinter der Firewall) an frimp@webmail.de. mail.gmx.de reicht die Mail an
|
||||
mail.webmail.de im anderen Netz weiter, bei 22 s holt frimp (Notebook#2)
|
||||
ab.
|
||||
* `gnutella` (zwei_rechnernetze_komplett): Filius' Gnutella-Programm auf
|
||||
Notebook 1.10, Notebook 1.11 und DNS-Server 2.10 (der „Lied der
|
||||
Woche.txt“ teilt), je höchstens drei Teilnehmer. 1.10 tritt bei 2.10 bei,
|
||||
dann 1.11 bei 1.10: Der Ping geht über 1.10 weiter zu 2.10, der Pong auf
|
||||
demselben Weg zurück. 1.10 sucht „woche“ (2.10 reicht die Query an 1.11
|
||||
weiter) und holt den Treffer über den Router.
|
||||
|
||||
Aufnahmelängen 12 s (`web`), 40 s (`email`), 26 s (`gnutella`): Das Ende
|
||||
liegt jeweils, wo nichts mehr geschieht. Bei 34 s fiel Javas letzter Abbau
|
||||
einer Verbindung knapp hinter das Ende, bei 14 s ein wiederholtes SYN.
|
||||
|
||||
Gemessen (03.10.2026):
|
||||
|
||||
* Die Datei hält die Konten der E-Mail-Programme nur in der älteren
|
||||
`kontoListe`, die Filius 2.14 nicht mehr liest: Nach dem Laden hat Java
|
||||
kein Konto. Der Leser übernimmt sie (`importEmail.ts`). Das Orakel trägt
|
||||
sie so ein, wie man sie im Kontodialog eintippt, mit den Werten der
|
||||
Liste.
|
||||
* Notebook 1.11 in zwei_rechnernetze hat kein Gateway (mit Absicht im
|
||||
Beispiel). Lernt es über einen Pong einen Teilnehmer im anderen Netz,
|
||||
bleibt in Java der Thread jeder Verbindung dorthin in
|
||||
`TCPSocket.verbinden` hängen: kein SYN auf der Leitung, keine Meldung, ein
|
||||
Download kommt nie an. Der Ablauf lässt darum 1.10 suchen.
|
||||
* Jeder TCP-Socket zieht beim Anlegen die nächste Anfangsnummer (ein
|
||||
gemeinsamer Zähler). Ein Server legt seinen ersten lauschenden Socket beim
|
||||
Start in seinem eigenen Thread an (`ServerAnwendung.annehmenVerbindungen`):
|
||||
Welcher Server eines Beispiels welche Nummer zieht, entscheidet der Takt
|
||||
der Threads. Verglichen wird unterhalb der Millionen.
|
||||
* Eine Suche geht zugleich an beide Teilnehmer von 1.10. An seiner Karte
|
||||
kommen die Frames der zwei Verbindungen in beliebiger Folge, einige zehn
|
||||
Millisekunden auseinander (in einer Aufnahme vertauscht, in der nächsten
|
||||
nicht).
|
||||
* 2.10 reicht die Query an 1.11 weiter, das ohne Gateway nicht antworten
|
||||
kann: Java schickt das SYN einmal und gibt nach einer Wartezeit auf (wie
|
||||
in `closedPort` von `TcpRun`); 1.11 seinerseits erreicht 2.10 gar nicht
|
||||
(s. o., kein Frame).
|
||||
* **Gleich lange RIP-Wege, gemessen**: Router #3 erreicht das Schulnetz
|
||||
(42.0.0.0) über Router `""` oder über #2, je zwei Sprünge. Jede Antwort
|
||||
an die Schule läuft dort durch, die des DNS 8.8.8.8 wie die von webmail.
|
||||
In beiden ruhigen Aufnahmen nahm Java #2, der Kern `""` (die zuerst
|
||||
gehörte Route); über #2 kosten zwei neue ARP-Suchen rund 270 ms. Der Test
|
||||
gibt dem Kern Javas Wahl, wie eine von Java gezogene Zufallszahl, und nur
|
||||
bei gleicher Länge.
|
||||
* RIP-Beacons kommen alle 30 s nach Zufallspausen, so viele, wie ein Router
|
||||
seit dem Start gezogen hat (s. RIP oben); verglichen werden in den
|
||||
Klassikern die Frames der Programme, RIP nicht.
|
||||
* Java fällt an jedem Verbindungsaufbau um einen Blick (rund 80 ms) zurück,
|
||||
dazwischen um wenige Millisekunden je Sekunde: In der E-Mail-Geschichte
|
||||
folgt die Weitergabe gmx → webmail auf herberts Verbindung, ihre Frames
|
||||
liegen bis zu 272 ms hinter dem Kern.
|
||||
|
||||
@@ -322,9 +322,12 @@ for run in pingLan:8000 pingRouted:13000 pingNobody:49000 pingUnreachable:45000
|
||||
done
|
||||
|
||||
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
|
||||
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts).
|
||||
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts)
|
||||
# and the classics of F3 (web, email, gnutella; packages/fls/test/classicsJava.test.ts).
|
||||
for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \
|
||||
nat_with_mapped_port:nat:15000 Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE:firewall:16000; do
|
||||
nat_with_mapped_port:nat:15000 Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE:firewall:16000 \
|
||||
internetwork_komplett:web:12000 Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE:email:40000 \
|
||||
zwei_rechnernetze_komplett:gnutella:26000; do
|
||||
file="${run%%:*}"; rest="${run#*:}"; story="${rest%%:*}"
|
||||
output="$root/packages/fls/test/examples/$story.java.json"
|
||||
rm -f "$output"
|
||||
|
||||
@@ -22,9 +22,43 @@
|
||||
// firewall's WAN address. Targets with one shortest RIP path
|
||||
// each: where two are equal, Java takes whichever its threads
|
||||
// learn first
|
||||
//
|
||||
// The classics of the classroom (the acceptance of F3), with what each
|
||||
// program told its window ("programs": time, program, text) and where each
|
||||
// ended ("end"):
|
||||
// web internetwork_komplett: Notebook 1.10 types www.filius.de into
|
||||
// its browser; the name is asked of DNS-Server 2.10 in another
|
||||
// network, the page comes from Webserver 5.10, two routers away
|
||||
// email Internet_Komplett_…_DE: when the DHCP clients have their
|
||||
// addresses (8 s), herbert@gmx.de (the first Notebook, in the
|
||||
// school's network behind the firewall) writes to
|
||||
// frimp@webmail.de; mail.gmx.de hands it on to mail.webmail.de
|
||||
// in another network; at 22 s frimp (Notebook#2) fetches. The
|
||||
// file keeps the programs' accounts only in the older kontoListe,
|
||||
// which FILIUS 2.14 no longer reads (the reader takes it,
|
||||
// importEmail.ts): they are set as one types them into the
|
||||
// program's account dialog, with the values the list holds
|
||||
// gnutella zwei_rechnernetze_komplett with FILIUS' Gnutella program on
|
||||
// Notebook 1.10, Notebook 1.11 and DNS-Server 2.10 (in the other
|
||||
// network; it shares "Lied der Woche.txt"), each keeping three
|
||||
// peers: 1.10 joins 2.10; 1.11 joins 1.10 (its ping goes on to
|
||||
// 2.10, the pong comes back through 1.10); 1.10 searches "woche"
|
||||
// (1.11 passes the query on) and downloads the hit through the
|
||||
// router (1.11 has no gateway: it could not fetch from the other
|
||||
// network)
|
||||
|
||||
import filius.hardware.knoten.Knoten;
|
||||
import filius.software.dateiaustausch.PeerToPeerAnwendung;
|
||||
import filius.software.dns.Resolver;
|
||||
import filius.software.email.Email;
|
||||
import filius.software.email.EmailAnwendung;
|
||||
import filius.software.email.EmailKonto;
|
||||
import filius.software.email.EmailServer;
|
||||
import filius.software.email.EmailUtils;
|
||||
import filius.software.system.Datei;
|
||||
import filius.software.www.HTTPNachricht;
|
||||
import filius.software.www.WebBrowser;
|
||||
import filius.software.www.WebServer;
|
||||
import filius.software.rip.RIPRoute;
|
||||
import filius.software.rip.RIPTable;
|
||||
import filius.software.system.VermittlungsrechnerBetriebssystem;
|
||||
@@ -72,6 +106,58 @@ public class ExampleRun {
|
||||
events.add(new Object[] { System.currentTimeMillis() - network.start, what });
|
||||
}
|
||||
|
||||
/** What a program told its window: time, program, text. */
|
||||
static final List<Object[]> told = new ArrayList<>();
|
||||
|
||||
static synchronized void tell(String program, String text) {
|
||||
told.add(new Object[] { System.currentTimeMillis() - network.start, program, text });
|
||||
}
|
||||
|
||||
static <T> T program(String node, String name) {
|
||||
@SuppressWarnings("unchecked")
|
||||
T program = (T) network.system(node).holeSoftware(name);
|
||||
return program;
|
||||
}
|
||||
|
||||
static void watchServer(String node, String program) {
|
||||
filius.software.Anwendung server = program(node, program.equals("webServer") ? "filius.software.www.WebServer" : "filius.software.email.EmailServer");
|
||||
server.hinzuBeobachter((observable, data) -> tell(node, String.valueOf(data)));
|
||||
}
|
||||
|
||||
/** GUIApplicationEmailAnwendungWindow's send button, as EmailClientRun makes it. */
|
||||
static void send(EmailAnwendung program, EmailKonto account, String to, String subject, String text) {
|
||||
Email mail = new Email();
|
||||
mail.setAbsender(account.getVorname() + (!account.getNachname().isEmpty() ? (" " + account.getNachname()) : "") + " <"
|
||||
+ account.getEmailAdresse() + ">");
|
||||
mail.setEmpfaenger(EmailUtils.stringToAddressEntryList(to));
|
||||
mail.setCc(EmailUtils.stringToAddressEntryList(""));
|
||||
mail.setBcc(EmailUtils.stringToAddressEntryList(""));
|
||||
mail.setBetreff(subject);
|
||||
mail.setText(text);
|
||||
program.versendeEmail(account.getSmtpserver(), mail, account.getEmailAdresse());
|
||||
}
|
||||
|
||||
static EmailKonto account(EmailAnwendung program) {
|
||||
return program.holeKontoListe().values().iterator().next();
|
||||
}
|
||||
|
||||
/** The account dialog's fields: user, password, first name, last name, address, and the server for both POP3 and SMTP. */
|
||||
static void setAccount(EmailAnwendung program, String user, String password, String first, String last, String address, String server) {
|
||||
EmailKonto account = new EmailKonto();
|
||||
account.setBenutzername(user);
|
||||
account.setPasswort(password);
|
||||
account.setVorname(first);
|
||||
account.setNachname(last);
|
||||
account.setEmailAdresse(address);
|
||||
account.setPop3server(server);
|
||||
account.setPop3port("110");
|
||||
account.setSmtpserver(server);
|
||||
account.setSmtpport("25");
|
||||
program.setzeKonto(account);
|
||||
}
|
||||
|
||||
static final String[] GNUTELLA_PEERS = { "Notebook 1.10", "Notebook 1.11", "DNS-Server 2.10" };
|
||||
|
||||
interface Call {
|
||||
String run() throws Exception;
|
||||
}
|
||||
@@ -101,6 +187,40 @@ public class ExampleRun {
|
||||
network = ScenarioNetwork.fromFile(args[0]);
|
||||
String story = args[1];
|
||||
long end = Long.parseLong(args[2]);
|
||||
if (story.equals("web")) {
|
||||
watchServer("Webserver 5.10", "webServer");
|
||||
WebBrowser browser = program("Notebook 1.10", "filius.software.www.WebBrowser");
|
||||
browser.hinzuBeobachter((observable, data) -> {
|
||||
if (data instanceof HTTPNachricht) {
|
||||
HTTPNachricht page = (HTTPNachricht) data;
|
||||
tell("browser", "page " + page.getStatusCode() + " " + page.getContentType() + "\n" + page.getDaten());
|
||||
} else {
|
||||
tell("browser", data == null ? "update" : String.valueOf(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (story.equals("email")) {
|
||||
watchServer("mail.gmx.de (130.100.200.25)", "emailServer");
|
||||
watchServer("mail.webmail.de (140.100.200.25)", "emailServer");
|
||||
setAccount(program("Notebook", "filius.software.email.EmailAnwendung"), "herbert", "herbert1234", "Herbert", "Schmidt", "herbert@gmx.de", "mail.gmx.de");
|
||||
setAccount(program("Notebook#2", "filius.software.email.EmailAnwendung"), "frimp", "frimp1234", "Frimp", "Mueller", "frimp@webmail.de", "mail.webmail.de");
|
||||
for (String node : new String[] { "Notebook", "Notebook#2" }) {
|
||||
EmailAnwendung client = program(node, "filius.software.email.EmailAnwendung");
|
||||
client.hinzuBeobachter((observable, data) -> tell(node, String.valueOf(data)));
|
||||
}
|
||||
}
|
||||
if (story.equals("gnutella")) {
|
||||
for (String node : GNUTELLA_PEERS) {
|
||||
network.system(node).installApp("filius.software.dateiaustausch.PeerToPeerAnwendung", false);
|
||||
PeerToPeerAnwendung peer = program(node, "filius.software.dateiaustausch.PeerToPeerAnwendung");
|
||||
peer.setMaxTeilnehmerZahl(3);
|
||||
peer.hinzuBeobachter((observable, data) -> {
|
||||
tell(node, "peers " + String.join(",", peer.holeBekanntePeerToPeerTeilnehmer()));
|
||||
tell(node, "results " + String.join("|", peer.holeErgebnisse()));
|
||||
});
|
||||
}
|
||||
network.system("DNS-Server 2.10").getDateisystem().speicherDatei("/peer2peer", new Datei("Lied der Woche.txt", "txt", "La la la"));
|
||||
}
|
||||
network.start();
|
||||
Thread poller = new Thread(() -> {
|
||||
while (true) {
|
||||
@@ -136,6 +256,42 @@ public class ExampleRun {
|
||||
ping("Notebook", "130.100.200.25", 2);
|
||||
ping("mail.gmx.de (130.100.200.25)", "42.0.0.10", 3);
|
||||
break;
|
||||
case "web": {
|
||||
tell("browser", "call holeWebseite www.filius.de");
|
||||
WebBrowser browser = program("Notebook 1.10", "filius.software.www.WebBrowser");
|
||||
browser.holeWebseite(WebRun.url("www.filius.de"));
|
||||
break;
|
||||
}
|
||||
case "email": {
|
||||
Thread.sleep(7500);
|
||||
EmailAnwendung herbert = program("Notebook", "filius.software.email.EmailAnwendung");
|
||||
tell("Notebook", "call versendeEmail frimp@webmail.de");
|
||||
send(herbert, account(herbert), "frimp@webmail.de", "Treffen", "Hallo Frimp,\nbis morgen!\nHerbert");
|
||||
long until = network.start + 22000 - System.currentTimeMillis();
|
||||
if (until > 0) Thread.sleep(until);
|
||||
EmailAnwendung frimp = program("Notebook#2", "filius.software.email.EmailAnwendung");
|
||||
EmailKonto account = account(frimp);
|
||||
tell("Notebook#2", "call emailsAbholenEmails");
|
||||
frimp.emailsAbholenEmails(account.getBenutzername(), account.getPasswort(), account.getPop3port(), account.getPop3server());
|
||||
break;
|
||||
}
|
||||
case "gnutella": {
|
||||
PeerToPeerAnwendung searching = program("Notebook 1.10", "filius.software.dateiaustausch.PeerToPeerAnwendung");
|
||||
PeerToPeerAnwendung near = program("Notebook 1.11", "filius.software.dateiaustausch.PeerToPeerAnwendung");
|
||||
tell("Notebook 1.10", "call join 141.99.2.10");
|
||||
searching.beitretenNetzwerk("141.99.2.10");
|
||||
Thread.sleep(network.start + 3000 - System.currentTimeMillis());
|
||||
tell("Notebook 1.11", "call join 141.99.1.10");
|
||||
near.beitretenNetzwerk("141.99.1.10");
|
||||
Thread.sleep(network.start + 5500 - System.currentTimeMillis());
|
||||
tell("Notebook 1.10", "call search woche");
|
||||
searching.sucheDatei("woche");
|
||||
Thread.sleep(network.start + 8500 - System.currentTimeMillis());
|
||||
tell("Notebook 1.10", "call download 0");
|
||||
if (searching.holeErgebnisse().isEmpty()) tell("Notebook 1.10", "skipped download 0");
|
||||
else searching.herunterladenDatei(0);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("unknown story " + story);
|
||||
}
|
||||
@@ -157,6 +313,48 @@ public class ExampleRun {
|
||||
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']');
|
||||
out.close(']');
|
||||
}
|
||||
synchronized (ExampleRun.class) {
|
||||
out.key("programs").open('[');
|
||||
for (Object[] each : told) out.open('[').number((Long) each[0]).string((String) each[1]).string((String) each[2]).close(']');
|
||||
out.close(']');
|
||||
}
|
||||
// Where the programs ended: each e-mail server's accounts with their mails, each e-mail program's
|
||||
// inbox and sent mails (as EmailClientRun writes them); each Gnutella program's peers, results and files.
|
||||
out.key("end").open('{');
|
||||
if (story.equals("email")) {
|
||||
for (String node : new String[] { "mail.gmx.de (130.100.200.25)", "mail.webmail.de (140.100.200.25)" }) {
|
||||
out.key(node);
|
||||
EmailClientRun.accounts(out, program(node, "filius.software.email.EmailServer"));
|
||||
}
|
||||
for (String node : new String[] { "Notebook", "Notebook#2" }) {
|
||||
EmailAnwendung client = program(node, "filius.software.email.EmailAnwendung");
|
||||
out.key(node).open('{');
|
||||
out.key("inbox");
|
||||
EmailClientRun.mails(out, client.getEmpfangeneNachrichten());
|
||||
out.key("sent");
|
||||
EmailClientRun.mails(out, client.getGesendeteNachrichten());
|
||||
out.close('}');
|
||||
}
|
||||
}
|
||||
if (story.equals("gnutella")) {
|
||||
for (String node : GNUTELLA_PEERS) {
|
||||
PeerToPeerAnwendung peer = program(node, "filius.software.dateiaustausch.PeerToPeerAnwendung");
|
||||
out.key(node).open('{');
|
||||
out.key("peers").open('[');
|
||||
for (String each : peer.holeBekanntePeerToPeerTeilnehmer()) out.string(each);
|
||||
out.close(']');
|
||||
out.key("results").open('[');
|
||||
for (String each : peer.holeErgebnisse()) out.string(each);
|
||||
out.close(']');
|
||||
out.key("files").open('[');
|
||||
for (Datei file : network.system(node).getDateisystem().holeDateien(peer.holeVerzeichnis())) {
|
||||
out.open('[').string(file.getName()).string(file.getDateiTyp()).string(file.getDateiInhalt()).close(']');
|
||||
}
|
||||
out.close(']');
|
||||
out.close('}');
|
||||
}
|
||||
}
|
||||
out.close('}');
|
||||
out.close('}');
|
||||
network.stop();
|
||||
ScenarioNetwork.write(args[3], out);
|
||||
|
||||
Reference in New Issue
Block a user