summaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/examples/CreateAndVerifyRaftSimulation.java142
-rw-r--r--src/main/java/examples/CreateMinimalRaftSimulation.java86
-rw-r--r--src/main/java/examples/CreateSimpleRaftSimulation.java122
-rw-r--r--src/main/java/examples/CreateWorkingRaftSimulation.java152
-rw-r--r--src/main/java/examples/RaftSimulationBuilder.java76
-rw-r--r--src/main/java/examples/TestRaftLoading.java57
-rw-r--r--src/main/java/protocols/implementations/VSRaftProtocol.java33
-rw-r--r--src/main/java/testing/CleanHeadlessRunner.java109
-rw-r--r--src/main/java/testing/DummySimulatorFrame.java93
-rw-r--r--src/main/java/testing/HeadlessSimulationRunner.java188
-rw-r--r--src/main/java/testing/LogCapture.java158
-rw-r--r--src/main/java/testing/LogEntry.java73
-rw-r--r--src/main/java/testing/LogListener.java14
-rw-r--r--src/main/java/testing/LogType.java21
-rw-r--r--src/main/java/testing/ProtocolTestRunner.java220
-rw-r--r--src/main/java/testing/ProtocolTestRunnerWithLogs.java114
-rw-r--r--src/main/java/testing/ProtocolVerifier.java243
-rw-r--r--src/main/java/testing/QuietProtocolTestRunner.java79
-rw-r--r--src/main/java/testing/RuleResult.java40
-rw-r--r--src/main/java/testing/SimulationMetrics.java47
-rw-r--r--src/main/java/testing/SimulationResult.java94
-rw-r--r--src/main/java/testing/VerificationResult.java57
-rw-r--r--src/main/java/testing/VerificationRule.java26
-rw-r--r--src/main/java/testing/examples/InteractiveTest.java66
-rw-r--r--src/main/java/testing/examples/QuickTest.java40
-rw-r--r--src/main/java/testing/examples/TestPingPongSimulation.java138
-rw-r--r--src/main/java/testing/examples/TestPingPongVerified.java132
27 files changed, 2613 insertions, 7 deletions
diff --git a/src/main/java/examples/CreateAndVerifyRaftSimulation.java b/src/main/java/examples/CreateAndVerifyRaftSimulation.java
new file mode 100644
index 0000000..126c37c
--- /dev/null
+++ b/src/main/java/examples/CreateAndVerifyRaftSimulation.java
@@ -0,0 +1,142 @@
+package examples;
+
+import simulator.*;
+import core.*;
+import prefs.*;
+import events.*;
+import events.internal.*;
+import events.implementations.*;
+import serialize.VSSerialize;
+import java.io.*;
+
+/**
+ * Creates a Raft simulation and verifies it can be loaded properly.
+ */
+public class CreateAndVerifyRaftSimulation {
+
+ private static final String RAFT_PROTOCOL = "protocols.implementations.VSRaftProtocol";
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("=== Creating and Verifying Raft Simulation ===\n");
+
+ // Initialize
+ VSDefaultPrefs prefs = new VSDefaultPrefs();
+ prefs.fillWithDefaults();
+ VSRegisteredEvents.init(prefs);
+
+ // Step 1: Create the simulation
+ System.out.println("Step 1: Creating Raft simulation...");
+
+ VSSimulatorFrame frame = new VSSimulatorFrame(prefs, null);
+ VSSimulator simulator = new VSSimulator(prefs, frame);
+ frame.addSimulator(simulator);
+
+ // Access visualization
+ java.lang.reflect.Field vizField = VSSimulator.class.getDeclaredField("simulatorVisualization");
+ vizField.setAccessible(true);
+ VSSimulatorVisualization viz = (VSSimulatorVisualization) vizField.get(simulator);
+
+ // Add processes (5 total: 3 servers + 2 clients)
+ while (viz.getNumProcesses() < 5) {
+ java.lang.reflect.Method addProcessMethod = VSSimulatorVisualization.class.getDeclaredMethod("addProcess");
+ addProcessMethod.setAccessible(true);
+ addProcessMethod.invoke(viz);
+ }
+
+ VSTaskManager taskManager = viz.getTaskManager();
+
+ // Add Raft server activations
+ System.out.println(" - Adding 3 Raft servers");
+ for (int i = 0; i < 3; i++) {
+ VSProtocolEvent serverEvent = new VSProtocolEvent();
+ serverEvent.setProtocolClassname(RAFT_PROTOCOL);
+ serverEvent.isClientProtocol(false);
+ serverEvent.isProtocolActivation(true);
+
+ VSTask task = new VSTask(0, viz.getProcess(i), serverEvent, false);
+ taskManager.addTask(task);
+ }
+
+ // Add Raft client activations
+ System.out.println(" - Adding 2 Raft clients");
+ for (int i = 3; i < 5; i++) {
+ VSProtocolEvent clientEvent = new VSProtocolEvent();
+ clientEvent.setProtocolClassname(RAFT_PROTOCOL);
+ clientEvent.isClientProtocol(true);
+ clientEvent.isProtocolActivation(true);
+
+ // Stagger client starts
+ VSTask task = new VSTask(200 + (i-3)*100, viz.getProcess(i), clientEvent, false);
+ taskManager.addTask(task);
+ }
+
+ // Add some events
+ System.out.println(" - Adding crash/recovery events");
+
+ // Server 0 crashes at 1000, recovers at 1500
+ VSProcessCrashEvent crash = new VSProcessCrashEvent();
+ taskManager.addTask(new VSTask(1000, viz.getProcess(0), crash, false));
+
+ VSProcessRecoverEvent recover = new VSProcessRecoverEvent();
+ taskManager.addTask(new VSTask(1500, viz.getProcess(0), recover, false));
+
+ // Save simulation
+ File outputFile = new File("saved-simulations/raft-verified.dat");
+ outputFile.getParentFile().mkdirs();
+
+ VSSerialize serialize = new VSSerialize();
+ serialize.saveSimulator(outputFile.getAbsolutePath(), simulator);
+
+ frame.dispose();
+
+ System.out.println(" ✓ Simulation saved to: " + outputFile.getName());
+
+ // Step 2: Verify the simulation can be loaded
+ System.out.println("\nStep 2: Loading and verifying simulation...");
+
+ VSSimulatorFrame frame2 = new VSSimulatorFrame(prefs, null);
+ VSSimulator loadedSim = serialize.openSimulator(outputFile.getAbsolutePath(), frame2);
+
+ if (loadedSim == null) {
+ System.err.println(" ✗ Failed to load simulation!");
+ System.exit(1);
+ }
+
+ // Verify contents
+ vizField = VSSimulator.class.getDeclaredField("simulatorVisualization");
+ vizField.setAccessible(true);
+ VSSimulatorVisualization loadedViz = (VSSimulatorVisualization) vizField.get(loadedSim);
+
+ System.out.println(" ✓ Simulation loaded successfully");
+ System.out.println(" - Processes: " + loadedViz.getNumProcesses());
+
+ // Check tasks
+ VSTaskManager loadedTaskManager = loadedViz.getTaskManager();
+ java.lang.reflect.Field tasksField = VSTaskManager.class.getDeclaredField("tasks");
+ tasksField.setAccessible(true);
+ Object taskQueue = tasksField.get(loadedTaskManager);
+ java.lang.reflect.Method sizeMethod = taskQueue.getClass().getMethod("size");
+ int taskCount = (Integer) sizeMethod.invoke(taskQueue);
+
+ System.out.println(" - Scheduled tasks: " + taskCount);
+
+ frame2.dispose();
+
+ // Step 3: Provide instructions
+ System.out.println("\n=== Success! ===");
+ System.out.println("\nTo run the Raft simulation:");
+ System.out.println("1. Start the simulator:");
+ System.out.println(" java -jar target/ds-sim-1.0.1-SNAPSHOT.jar");
+ System.out.println("\n2. Load the simulation:");
+ System.out.println(" File → Open → saved-simulations/raft-verified.dat");
+ System.out.println("\n3. Run the simulation:");
+ System.out.println(" Click the 'Run' button (▶)");
+ System.out.println("\n4. What to look for:");
+ System.out.println(" - Leader election messages (REQUEST_VOTE, VOTE_RESPONSE)");
+ System.out.println(" - Heartbeats from leader (APPEND_ENTRIES)");
+ System.out.println(" - Client requests and responses");
+ System.out.println(" - Re-election when server 0 crashes at time 1000");
+
+ System.exit(0);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/examples/CreateMinimalRaftSimulation.java b/src/main/java/examples/CreateMinimalRaftSimulation.java
new file mode 100644
index 0000000..62db468
--- /dev/null
+++ b/src/main/java/examples/CreateMinimalRaftSimulation.java
@@ -0,0 +1,86 @@
+package examples;
+
+import simulator.*;
+import core.*;
+import prefs.*;
+import events.*;
+import events.internal.*;
+import serialize.VSSerialize;
+import java.io.*;
+import java.lang.reflect.*;
+
+/**
+ * Creates a minimal Raft simulation with just protocol activations.
+ * This tests if the basic simulation saving/loading works.
+ */
+public class CreateMinimalRaftSimulation {
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("=== Creating Minimal Raft Simulation ===\n");
+
+ // Initialize
+ VSDefaultPrefs prefs = new VSDefaultPrefs();
+ prefs.fillWithDefaults();
+ VSRegisteredEvents.init(prefs);
+
+ // Create simulator without GUI
+ VSSimulatorFrame frame = new VSSimulatorFrame(prefs, null);
+ VSSimulator simulator = new VSSimulator(prefs, frame);
+ frame.addSimulator(simulator);
+
+ // Access visualization via reflection
+ Field vizField = VSSimulator.class.getDeclaredField("simulatorVisualization");
+ vizField.setAccessible(true);
+ VSSimulatorVisualization viz = (VSSimulatorVisualization) vizField.get(simulator);
+
+ // Add 3 processes
+ Method addProcessMethod = VSSimulatorVisualization.class.getDeclaredMethod("addProcess");
+ addProcessMethod.setAccessible(true);
+ for (int i = 0; i < 3; i++) {
+ addProcessMethod.invoke(viz);
+ }
+
+ VSTaskManager taskManager = viz.getTaskManager();
+
+ // Create only one Raft server activation at time 0
+ System.out.println("Adding single Raft server activation on process 0...");
+ VSProtocolEvent serverEvent = new VSProtocolEvent();
+ serverEvent.setProtocolClassname("protocols.implementations.VSRaftProtocol");
+ serverEvent.isClientProtocol(false);
+ serverEvent.isProtocolActivation(true);
+
+ VSTask task = new VSTask(0, viz.getProcess(0), serverEvent, false);
+ taskManager.addTask(task);
+
+ // Save simulation
+ File outputFile = new File("saved-simulations/raft-minimal.dat");
+ outputFile.getParentFile().mkdirs();
+
+ VSSerialize serialize = new VSSerialize();
+ serialize.saveSimulator(outputFile.getAbsolutePath(), simulator);
+
+ frame.dispose();
+
+ System.out.println("\nSimulation saved to: " + outputFile.getAbsolutePath());
+ System.out.println("\nTo test:");
+ System.out.println("1. Run: java -jar target/ds-sim-1.0.1-SNAPSHOT.jar");
+ System.out.println("2. File → Open → saved-simulations/raft-minimal.dat");
+ System.out.println("3. Click Run button and check the logs");
+
+ // Try to immediately load it back to verify
+ System.out.println("\nVerifying saved file can be loaded...");
+ try {
+ VSSimulatorFrame frame2 = new VSSimulatorFrame(prefs, null);
+ VSSimulator loaded = serialize.openSimulator(outputFile.getAbsolutePath(), frame2);
+ if (loaded != null) {
+ System.out.println("✓ File loaded successfully!");
+ frame2.dispose();
+ } else {
+ System.out.println("✗ Failed to load file!");
+ }
+ } catch (Exception e) {
+ System.out.println("✗ Error loading file: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/examples/CreateSimpleRaftSimulation.java b/src/main/java/examples/CreateSimpleRaftSimulation.java
new file mode 100644
index 0000000..278824d
--- /dev/null
+++ b/src/main/java/examples/CreateSimpleRaftSimulation.java
@@ -0,0 +1,122 @@
+package examples;
+
+import simulator.*;
+import core.*;
+import prefs.*;
+import events.*;
+import events.internal.*;
+import events.implementations.*;
+import serialize.VSSerialize;
+import java.io.*;
+
+/**
+ * Creates a simple working Raft simulation.
+ * The key insight: Raft protocol uses HAS_ON_SERVER_START, so servers
+ * automatically start when activated. We just need to activate them!
+ */
+public class CreateSimpleRaftSimulation {
+
+ private static final String RAFT_PROTOCOL = "protocols.implementations.VSRaftProtocol";
+
+ public static void main(String[] args) throws Exception {
+ // Initialize
+ VSDefaultPrefs prefs = new VSDefaultPrefs();
+ prefs.fillWithDefaults();
+ VSRegisteredEvents.init(prefs);
+
+ // Create frame and simulator
+ VSSimulatorFrame frame = new VSSimulatorFrame(prefs, null);
+ VSSimulator simulator = new VSSimulator(prefs, frame);
+ frame.addSimulator(simulator);
+
+ // Access visualization via reflection
+ java.lang.reflect.Field vizField = VSSimulator.class.getDeclaredField("simulatorVisualization");
+ vizField.setAccessible(true);
+ VSSimulatorVisualization viz = (VSSimulatorVisualization) vizField.get(simulator);
+
+ // Add more processes - we want 5 total (3 servers, 2 clients)
+ while (viz.getNumProcesses() < 5) {
+ java.lang.reflect.Method addProcessMethod = VSSimulatorVisualization.class.getDeclaredMethod("addProcess");
+ addProcessMethod.setAccessible(true);
+ addProcessMethod.invoke(viz);
+ }
+
+ VSTaskManager taskManager = viz.getTaskManager();
+
+ // Activate Raft SERVERS on processes 0, 1, 2
+ // Since Raft uses HAS_ON_SERVER_START, onServerStart() will be called automatically!
+ System.out.println("Creating Raft server activations...");
+ for (int i = 0; i < 3; i++) {
+ VSProtocolEvent serverEvent = new VSProtocolEvent();
+ serverEvent.setProtocolClassname(RAFT_PROTOCOL);
+ serverEvent.isClientProtocol(false); // Server mode
+ serverEvent.isProtocolActivation(true); // Activation
+
+ // Activate at time 0
+ VSTask task = new VSTask(0, viz.getProcess(i), serverEvent, false);
+ taskManager.addTask(task);
+ System.out.println(" - Server " + i + " will activate at time 0");
+ }
+
+ // Activate Raft CLIENTS on processes 3, 4
+ // Clients will react to server heartbeats and start sending requests
+ System.out.println("\nCreating Raft client activations...");
+ for (int i = 3; i < 5; i++) {
+ VSProtocolEvent clientEvent = new VSProtocolEvent();
+ clientEvent.setProtocolClassname(RAFT_PROTOCOL);
+ clientEvent.isClientProtocol(true); // Client mode
+ clientEvent.isProtocolActivation(true); // Activation
+
+ // Activate clients a bit later so servers have time to elect leader
+ VSTask task = new VSTask(300 + (i-3)*100, viz.getProcess(i), clientEvent, false);
+ taskManager.addTask(task);
+ System.out.println(" - Client " + (i-3) + " will activate at time " + (300 + (i-3)*100));
+ }
+
+ // Add crash/recovery to demonstrate leader re-election
+ System.out.println("\nAdding failure scenarios...");
+
+ // Crash server 0 at time 1000
+ VSProcessCrashEvent crash = new VSProcessCrashEvent();
+ VSTask crashTask = new VSTask(1000, viz.getProcess(0), crash, false);
+ taskManager.addTask(crashTask);
+ System.out.println(" - Server 0 will crash at time 1000");
+
+ // Recover server 0 at time 1500
+ VSProcessRecoverEvent recover = new VSProcessRecoverEvent();
+ VSTask recoverTask = new VSTask(1500, viz.getProcess(0), recover, false);
+ taskManager.addTask(recoverTask);
+ System.out.println(" - Server 0 will recover at time 1500");
+
+ // Save simulation
+ File outputFile = new File("saved-simulations/raft-simple.dat");
+ outputFile.getParentFile().mkdirs();
+
+ VSSerialize serialize = new VSSerialize();
+ serialize.saveSimulator(outputFile.getAbsolutePath(), simulator);
+
+ frame.dispose();
+
+ System.out.println("\n===========================================");
+ System.out.println("Simple Raft simulation saved successfully!");
+ System.out.println("===========================================");
+ System.out.println("\nFile: " + outputFile.getAbsolutePath());
+ System.out.println("\nWhat happens in this simulation:");
+ System.out.println("1. Time 0: Three Raft servers start and begin leader election");
+ System.out.println("2. Time ~150-300: One server becomes leader (watch for election messages)");
+ System.out.println("3. Time 300: First client activates and starts sending requests");
+ System.out.println("4. Time 400: Second client activates and starts sending requests");
+ System.out.println("5. Time 1000: Server 0 crashes, triggering new leader election");
+ System.out.println("6. Time 1500: Server 0 recovers and rejoins as follower");
+ System.out.println("\nTo run the simulation:");
+ System.out.println("1. java -jar target/ds-sim-1.0.1-SNAPSHOT.jar");
+ System.out.println("2. File -> Open -> saved-simulations/raft-simple.dat");
+ System.out.println("3. Click 'Run' and watch the Raft consensus in action!");
+ System.out.println("\nLook for:");
+ System.out.println("- REQUEST_VOTE and VOTE_RESPONSE messages during elections");
+ System.out.println("- APPEND_ENTRIES messages (heartbeats) from leader");
+ System.out.println("- CLIENT_REQUEST messages and their processing");
+
+ System.exit(0);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/examples/CreateWorkingRaftSimulation.java b/src/main/java/examples/CreateWorkingRaftSimulation.java
new file mode 100644
index 0000000..0bc5df4
--- /dev/null
+++ b/src/main/java/examples/CreateWorkingRaftSimulation.java
@@ -0,0 +1,152 @@
+package examples;
+
+import simulator.*;
+import core.*;
+import prefs.*;
+import events.*;
+import events.internal.*;
+import events.implementations.*;
+import serialize.VSSerialize;
+import java.io.*;
+import java.lang.reflect.*;
+
+/**
+ * Creates a working Raft simulation by properly setting up the event queue
+ * and ensuring protocols are activated through the normal event system.
+ */
+public class CreateWorkingRaftSimulation {
+
+ private static final String RAFT_PROTOCOL = "protocols.implementations.VSRaftProtocol";
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("=== Creating Working Raft Simulation ===\n");
+
+ // Initialize
+ VSDefaultPrefs prefs = new VSDefaultPrefs();
+ prefs.fillWithDefaults();
+ VSRegisteredEvents.init(prefs);
+
+ // Create simulator with frame
+ VSSimulatorFrame frame = new VSSimulatorFrame(prefs, null);
+ VSSimulator simulator = new VSSimulator(prefs, frame);
+ frame.addSimulator(simulator);
+
+ // Access visualization
+ Field vizField = VSSimulator.class.getDeclaredField("simulatorVisualization");
+ vizField.setAccessible(true);
+ VSSimulatorVisualization viz = (VSSimulatorVisualization) vizField.get(simulator);
+
+ // Add 5 processes (3 servers + 2 clients)
+ Method addProcessMethod = VSSimulatorVisualization.class.getDeclaredMethod("addProcess");
+ addProcessMethod.setAccessible(true);
+ System.out.println("Adding 5 processes...");
+ for (int i = 0; i < 5; i++) {
+ addProcessMethod.invoke(viz);
+ }
+
+ VSTaskManager taskManager = viz.getTaskManager();
+
+ // Schedule Raft server activations at time 0
+ System.out.println("\nScheduling Raft server activations:");
+ for (int i = 0; i < 3; i++) {
+ VSProtocolEvent serverEvent = new VSProtocolEvent();
+ serverEvent.setProtocolClassname(RAFT_PROTOCOL);
+ serverEvent.isClientProtocol(false); // Server mode
+ serverEvent.isProtocolActivation(true); // This is an activation
+
+ VSTask task = new VSTask(0, viz.getProcess(i), serverEvent, false);
+ taskManager.addTask(task);
+ System.out.println(" - Server " + i + " activation scheduled at time 0");
+ }
+
+ // Schedule Raft client activations with slight delay
+ System.out.println("\nScheduling Raft client activations:");
+ for (int i = 3; i < 5; i++) {
+ VSProtocolEvent clientEvent = new VSProtocolEvent();
+ clientEvent.setProtocolClassname(RAFT_PROTOCOL);
+ clientEvent.isClientProtocol(true); // Client mode
+ clientEvent.isProtocolActivation(true); // This is an activation
+
+ // Start clients after servers have initialized
+ long startTime = 500 + (i - 3) * 200;
+ VSTask task = new VSTask(startTime, viz.getProcess(i), clientEvent, false);
+ taskManager.addTask(task);
+ System.out.println(" - Client " + (i-3) + " activation scheduled at time " + startTime);
+ }
+
+ // Add some interesting events
+ System.out.println("\nAdding crash/recovery events:");
+
+ // Process 0 crashes at time 2000 and recovers at 3000
+ VSProcessCrashEvent crash1 = new VSProcessCrashEvent();
+ taskManager.addTask(new VSTask(2000, viz.getProcess(0), crash1, false));
+ System.out.println(" - Server 0 crash scheduled at time 2000");
+
+ VSProcessRecoverEvent recover1 = new VSProcessRecoverEvent();
+ taskManager.addTask(new VSTask(3000, viz.getProcess(0), recover1, false));
+ System.out.println(" - Server 0 recovery scheduled at time 3000");
+
+ // Process 1 crashes at time 4000 and recovers at 5000
+ VSProcessCrashEvent crash2 = new VSProcessCrashEvent();
+ taskManager.addTask(new VSTask(4000, viz.getProcess(1), crash2, false));
+ System.out.println(" - Server 1 crash scheduled at time 4000");
+
+ VSProcessRecoverEvent recover2 = new VSProcessRecoverEvent();
+ taskManager.addTask(new VSTask(5000, viz.getProcess(1), recover2, false));
+ System.out.println(" - Server 1 recovery scheduled at time 5000");
+
+ // Save simulation
+ File outputFile = new File("saved-simulations/raft-working.dat");
+ outputFile.getParentFile().mkdirs();
+
+ System.out.println("\nSaving simulation...");
+ VSSerialize serialize = new VSSerialize();
+ serialize.saveSimulator(outputFile.getAbsolutePath(), simulator);
+
+ frame.dispose();
+
+ System.out.println("\n✓ Simulation saved to: " + outputFile.getAbsolutePath());
+
+ // Create instruction file
+ File instructionFile = new File("saved-simulations/README-raft.txt");
+ try (PrintWriter writer = new PrintWriter(instructionFile)) {
+ writer.println("RAFT CONSENSUS SIMULATION");
+ writer.println("========================");
+ writer.println();
+ writer.println("This directory contains Raft consensus protocol simulations:");
+ writer.println();
+ writer.println("1. raft-working.dat - Full working simulation with:");
+ writer.println(" - 3 Raft servers (processes 0-2)");
+ writer.println(" - 2 Raft clients (processes 3-4)");
+ writer.println(" - Server crash/recovery events");
+ writer.println();
+ writer.println("To run the simulation:");
+ writer.println("1. java -jar target/ds-sim-1.0.1-SNAPSHOT.jar");
+ writer.println("2. File → Open → saved-simulations/raft-working.dat");
+ writer.println("3. Click Run (▶) button");
+ writer.println();
+ writer.println("What to look for:");
+ writer.println("- Leader election (REQUEST_VOTE messages)");
+ writer.println("- Heartbeats from leader (APPEND_ENTRIES)");
+ writer.println("- Client requests and responses");
+ writer.println("- Re-election when servers crash");
+ writer.println();
+ writer.println("Timeline:");
+ writer.println("- Time 0: Servers start, begin leader election");
+ writer.println("- Time 500-700: Clients start");
+ writer.println("- Time 2000: Server 0 crashes");
+ writer.println("- Time 3000: Server 0 recovers");
+ writer.println("- Time 4000: Server 1 crashes");
+ writer.println("- Time 5000: Server 1 recovers");
+ }
+
+ System.out.println("✓ Instructions saved to: " + instructionFile.getAbsolutePath());
+
+ System.out.println("\n=== Success! ===");
+ System.out.println("\nThe Raft simulation has been created with the following setup:");
+ System.out.println("- 3 servers implementing Raft consensus");
+ System.out.println("- 2 clients that will send requests");
+ System.out.println("- Crash/recovery events to test fault tolerance");
+ System.out.println("\nRun the simulator and load the file to see it in action!");
+ }
+} \ No newline at end of file
diff --git a/src/main/java/examples/RaftSimulationBuilder.java b/src/main/java/examples/RaftSimulationBuilder.java
new file mode 100644
index 0000000..c802448
--- /dev/null
+++ b/src/main/java/examples/RaftSimulationBuilder.java
@@ -0,0 +1,76 @@
+package examples;
+
+import simulator.*;
+import core.*;
+import prefs.*;
+import events.*;
+import events.internal.*;
+import serialize.VSSerialize;
+import java.io.*;
+
+/**
+ * Builder for creating Raft simulations programmatically.
+ * Uses reflection to access private simulator fields when necessary.
+ */
+public class RaftSimulationBuilder {
+
+ private static final String RAFT_PROTOCOL = "protocols.implementations.VSRaftProtocol";
+
+ public static void main(String[] args) throws Exception {
+ // Initialize
+ VSDefaultPrefs prefs = new VSDefaultPrefs();
+ prefs.fillWithDefaults();
+ VSRegisteredEvents.init(prefs);
+
+ // Create frame and simulator
+ VSSimulatorFrame frame = new VSSimulatorFrame(prefs, null);
+ VSSimulator simulator = new VSSimulator(prefs, frame);
+ frame.addSimulator(simulator);
+
+ // Access private field via reflection
+ java.lang.reflect.Field vizField = VSSimulator.class.getDeclaredField("simulatorVisualization");
+ vizField.setAccessible(true);
+ VSSimulatorVisualization viz = (VSSimulatorVisualization) vizField.get(simulator);
+
+ // Build Raft simulation
+ VSTaskManager taskManager = viz.getTaskManager();
+
+ // Add server activations (processes 0,1)
+ for (int i = 0; i < 2; i++) {
+ VSProtocolEvent serverEvent = new VSProtocolEvent();
+ serverEvent.setProtocolClassname(RAFT_PROTOCOL);
+ serverEvent.isClientProtocol(false);
+ serverEvent.isProtocolActivation(true);
+
+ VSTask task = new VSTask(0, viz.getProcess(i), serverEvent, false);
+ taskManager.addTask(task);
+ }
+
+ // Add client activation (process 2)
+ VSProtocolEvent clientEvent = new VSProtocolEvent();
+ clientEvent.setProtocolClassname(RAFT_PROTOCOL);
+ clientEvent.isClientProtocol(true);
+ clientEvent.isProtocolActivation(true);
+
+ VSTask clientTask = new VSTask(100, viz.getProcess(2), clientEvent, false);
+ taskManager.addTask(clientTask);
+
+ // Save
+ File outputFile = new File("saved-simulations/raft-consensus.dat");
+ outputFile.getParentFile().mkdirs();
+
+ VSSerialize serialize = new VSSerialize();
+ serialize.saveSimulator(outputFile.getAbsolutePath(), simulator);
+
+ frame.dispose();
+
+ System.out.println("Raft simulation created: " + outputFile.getAbsolutePath());
+ System.out.println("\nContains:");
+ System.out.println("- 2 Raft servers (processes 0-1)");
+ System.out.println("- 1 Raft client (process 2)");
+ System.out.println("\nRun with: java -jar target/ds-sim-1.0.1-SNAPSHOT.jar");
+ System.out.println("Then open: " + outputFile.getName());
+
+ System.exit(0);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/examples/TestRaftLoading.java b/src/main/java/examples/TestRaftLoading.java
new file mode 100644
index 0000000..ebad379
--- /dev/null
+++ b/src/main/java/examples/TestRaftLoading.java
@@ -0,0 +1,57 @@
+package examples;
+
+import events.VSRegisteredEvents;
+import prefs.VSDefaultPrefs;
+import java.util.Vector;
+
+/**
+ * Test if Raft protocol is properly registered and loadable
+ */
+public class TestRaftLoading {
+ public static void main(String[] args) {
+ // Initialize
+ VSDefaultPrefs prefs = new VSDefaultPrefs();
+ prefs.fillWithDefaults();
+ VSRegisteredEvents.init(prefs);
+
+ // List all registered protocols
+ System.out.println("=== Registered Protocols ===");
+ Vector<String> protocolNames = VSRegisteredEvents.getProtocolNames();
+ for (String name : protocolNames) {
+ String className = VSRegisteredEvents.getClassnameByEventname(name);
+ System.out.println(name + " -> " + className);
+ }
+
+ System.out.println("\n=== Protocol Classnames ===");
+ Vector<String> protocolClassnames = VSRegisteredEvents.getProtocolClassnames();
+ for (String className : protocolClassnames) {
+ String shortName = VSRegisteredEvents.getShortnameByClassname(className);
+ System.out.println(className + " (short: " + shortName + ")");
+ }
+
+ // Check Raft specifically
+ System.out.println("\n=== Raft Protocol Check ===");
+ String raftClass = "protocols.implementations.VSRaftProtocol";
+ String raftShortName = VSRegisteredEvents.getShortnameByClassname(raftClass);
+ String raftEventName = VSRegisteredEvents.getNameByClassname(raftClass);
+
+ System.out.println("Class: " + raftClass);
+ System.out.println("Short name: " + raftShortName);
+ System.out.println("Event name: " + raftEventName);
+
+ // Try to load the class
+ try {
+ Class<?> clazz = Class.forName(raftClass);
+ System.out.println("Class loaded successfully: " + clazz.getName());
+
+ // Check if it's a protocol
+ if (protocols.VSAbstractProtocol.class.isAssignableFrom(clazz)) {
+ System.out.println("✓ Is a valid protocol class");
+ } else {
+ System.out.println("✗ NOT a protocol class!");
+ }
+ } catch (ClassNotFoundException e) {
+ System.out.println("✗ Class not found: " + e.getMessage());
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/protocols/implementations/VSRaftProtocol.java b/src/main/java/protocols/implementations/VSRaftProtocol.java
index 2029b72..0d8fa20 100644
--- a/src/main/java/protocols/implementations/VSRaftProtocol.java
+++ b/src/main/java/protocols/implementations/VSRaftProtocol.java
@@ -73,6 +73,10 @@ public class VSRaftProtocol extends VSAbstractProtocol {
private Integer currentLeader;
private long lastHeartbeat;
+ // Client state
+ private boolean clientHasScheduled = false;
+ private int clientRequestCount = 0;
+
/**
* Log entry structure
*/
@@ -185,19 +189,22 @@ public class VSRaftProtocol extends VSAbstractProtocol {
@Override
public void onClientInit() {
- // Clients don't need special initialization for Raft
- setBoolean("raft.client.enabled", true);
+ // Initialize client state
+ clientHasScheduled = false;
+ clientRequestCount = 0;
}
@Override
public void onClientStart() {
- // Schedule periodic client requests for testing
- scheduleAt(process.getTime() + 500);
+ // This method is never called when using HAS_ON_SERVER_START
+ // Clients will send requests in response to server heartbeats instead
}
@Override
public void onClientReset() {
removeSchedules();
+ clientHasScheduled = false;
+ clientRequestCount = 0;
}
@Override
@@ -208,6 +215,13 @@ public class VSRaftProtocol extends VSAbstractProtocol {
boolean success = message.getBoolean("success");
String result = message.getString("result");
raftLog("Client received response: success=" + success + ", result=" + result);
+ } else if (MSG_APPEND_ENTRIES.equals(msgType)) {
+ // Client receives heartbeat from leader - good time to send a request
+ if (!clientHasScheduled) {
+ clientHasScheduled = true;
+ // Schedule first client request after a short delay
+ scheduleAt(process.getTime() + 100);
+ }
}
}
@@ -221,10 +235,15 @@ public class VSRaftProtocol extends VSAbstractProtocol {
request.setLong("requestId", System.currentTimeMillis());
sendMessage(request);
- raftLog("Client sent request: " + request.getString("command"));
+ raftLog("Client sent request #" + clientRequestCount + ": " + request.getString("command"));
- // Schedule next request
- scheduleAt(process.getTime() + 1000 + process.getRandomPercentage() * 10);
+ // Update request count
+ clientRequestCount++;
+
+ // Schedule next request after a delay
+ if (clientRequestCount < 10) { // Limit number of requests for testing
+ scheduleAt(process.getTime() + 1000 + process.getRandomPercentage() * 10);
+ }
}
// --- Raft Algorithm Implementation ---
diff --git a/src/main/java/testing/CleanHeadlessRunner.java b/src/main/java/testing/CleanHeadlessRunner.java
new file mode 100644
index 0000000..94b4784
--- /dev/null
+++ b/src/main/java/testing/CleanHeadlessRunner.java
@@ -0,0 +1,109 @@
+package testing;
+
+import java.io.*;
+
+/**
+ * A clean headless test runner that suppresses ALL GUI-related errors internally.
+ */
+public class CleanHeadlessRunner {
+
+ public static void main(String[] args) {
+ // Redirect stderr to filter out GUI errors
+ PrintStream originalErr = System.err;
+ FilteringPrintStream filteringErr = new FilteringPrintStream(originalErr);
+ System.setErr(filteringErr);
+
+ try {
+ // Run the actual tests
+ ProtocolTestRunnerWithLogs.main(args);
+ } finally {
+ // Restore original stderr
+ System.setErr(originalErr);
+ }
+ }
+
+ /**
+ * A PrintStream that filters out GUI-related error messages.
+ */
+ private static class FilteringPrintStream extends PrintStream {
+ private final PrintStream original;
+ private boolean inStackTrace = false;
+
+ public FilteringPrintStream(PrintStream original) {
+ super(new FilteringOutputStream(original));
+ this.original = original;
+ ((FilteringOutputStream) out).setPrintStream(this);
+ }
+
+ @Override
+ public void println(String x) {
+ if (shouldFilter(x)) {
+ inStackTrace = true;
+ return;
+ }
+ if (inStackTrace && (x == null || x.trim().isEmpty() || !x.startsWith("\tat"))) {
+ inStackTrace = false;
+ }
+ if (!inStackTrace) {
+ super.println(x);
+ }
+ }
+
+ @Override
+ public void print(String s) {
+ if (!inStackTrace && !shouldFilter(s)) {
+ super.print(s);
+ }
+ }
+
+ private boolean shouldFilter(String message) {
+ if (message == null) return false;
+
+ return message.contains("Component must have a valid peer") ||
+ message.contains("java.lang.IllegalStateException") ||
+ message.contains("createBufferStrategy") ||
+ message.contains("FlipBufferStrategy") ||
+ message.contains("at java.desktop/") ||
+ message.contains("at simulator.VSSimulatorVisualization.paint") ||
+ message.contains("VSMessageLine.<init>") ||
+ message.contains("Error during simulation: null") ||
+ (message.startsWith("java.lang.") &&
+ message.contains("InvocationTargetException"));
+ }
+ }
+
+ /**
+ * Custom OutputStream for filtering.
+ */
+ private static class FilteringOutputStream extends OutputStream {
+ private final PrintStream target;
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ private FilteringPrintStream parent;
+
+ public FilteringOutputStream(PrintStream target) {
+ this.target = target;
+ }
+
+ public void setPrintStream(FilteringPrintStream parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ buffer.write(b);
+ if (b == '\n') {
+ String line = buffer.toString();
+ buffer.reset();
+
+ if (parent != null && !parent.shouldFilter(line)) {
+ target.print(line);
+ }
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ target.flush();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/DummySimulatorFrame.java b/src/main/java/testing/DummySimulatorFrame.java
new file mode 100644
index 0000000..b211851
--- /dev/null
+++ b/src/main/java/testing/DummySimulatorFrame.java
@@ -0,0 +1,93 @@
+package testing;
+
+import simulator.VSSimulatorFrame;
+import prefs.VSPrefs;
+import javax.swing.SwingUtilities;
+import java.awt.Dimension;
+import java.awt.Point;
+
+/**
+ * A minimal simulator frame for headless operation.
+ * Creates a real frame but immediately hides it and moves it off-screen.
+ */
+public class DummySimulatorFrame extends VSSimulatorFrame {
+
+ public DummySimulatorFrame(VSPrefs prefs) {
+ super(prefs, null); // null for relativeTo component
+
+ // Make the frame as small as possible and move off-screen
+ SwingUtilities.invokeLater(() -> {
+ setSize(1, 1);
+ setLocation(-1000, -1000);
+ setVisible(false);
+ });
+ }
+
+ @Override
+ public void resetCurrentSimulator() {
+ // Check if we have a current simulator before resetting
+ if (getCurrentSimulator() != null) {
+ // Only reset menu states, don't update GUI
+ getCurrentSimulator().getMenuItemStates().setStart(true);
+ getCurrentSimulator().getMenuItemStates().setPause(false);
+ getCurrentSimulator().getMenuItemStates().setReset(false);
+ getCurrentSimulator().getMenuItemStates().setReplay(false);
+ }
+ }
+
+ @Override
+ public void updateSimulatorMenu() {
+ // Do nothing - no menu updates in headless mode
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ // Always keep invisible
+ super.setVisible(false);
+ }
+
+ @Override
+ public void pack() {
+ // Set minimal size instead of packing
+ setSize(1, 1);
+ }
+
+ @Override
+ public void toFront() {
+ // Do nothing - don't bring to front
+ }
+
+ @Override
+ public void repaint() {
+ // Do nothing - no repainting needed
+ }
+
+ @Override
+ public void addSimulator(simulator.VSSimulator simulator) {
+ // Add simulator without triggering tab changes and painting
+ if (getSimulators() != null) {
+ getSimulators().add(simulator);
+ }
+ setCurrentSimulator(simulator);
+ }
+
+ protected void setCurrentSimulator(simulator.VSSimulator simulator) {
+ try {
+ java.lang.reflect.Field field = VSSimulatorFrame.class.getDeclaredField("currentSimulator");
+ field.setAccessible(true);
+ field.set(this, simulator);
+ } catch (Exception e) {
+ // Ignore errors
+ }
+ }
+
+ protected java.util.Vector<simulator.VSSimulator> getSimulators() {
+ try {
+ java.lang.reflect.Field field = VSSimulatorFrame.class.getDeclaredField("simulators");
+ field.setAccessible(true);
+ return (java.util.Vector<simulator.VSSimulator>) field.get(this);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/HeadlessSimulationRunner.java b/src/main/java/testing/HeadlessSimulationRunner.java
new file mode 100644
index 0000000..c3b699e
--- /dev/null
+++ b/src/main/java/testing/HeadlessSimulationRunner.java
@@ -0,0 +1,188 @@
+package testing;
+
+import simulator.*;
+import core.*;
+import prefs.*;
+import events.*;
+import serialize.VSSerialize;
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * Runs DS-Sim simulations in headless mode without GUI dependencies.
+ * Captures logs and provides verification capabilities for automated testing.
+ */
+public class HeadlessSimulationRunner {
+ private final VSDefaultPrefs prefs;
+ private VSSimulator simulator;
+ private VSSimulatorVisualization viz;
+ private LogCapture logCapture;
+ private final ExecutorService executor;
+ private boolean printLogs = false;
+
+ public HeadlessSimulationRunner() {
+ this.prefs = new VSDefaultPrefs();
+ this.prefs.fillWithDefaults();
+ VSRegisteredEvents.init(prefs);
+ this.executor = Executors.newSingleThreadExecutor();
+ }
+
+ /**
+ * Run a simulation from a saved file for a specified duration.
+ *
+ * @param simulationFile Path to the saved simulation .dat file
+ * @param maxTime Maximum simulation time in milliseconds
+ * @return SimulationResult containing logs and metrics
+ */
+ public SimulationResult runSimulation(String simulationFile, long maxTime)
+ throws Exception {
+ return runSimulation(simulationFile, maxTime, null);
+ }
+
+ /**
+ * Run a simulation with an optional log listener.
+ */
+ public SimulationResult runSimulation(String simulationFile, long maxTime, LogListener listener)
+ throws Exception {
+ System.out.println("Loading simulation: " + simulationFile);
+
+ try {
+ // Use the new headless loader
+ HeadlessLoader.LoadedSimulation loaded = HeadlessLoader.load(simulationFile, prefs);
+ simulator = loaded.getSimulator();
+ viz = loaded.getVisualization();
+
+ // Install log capture
+ logCapture = new LogCapture();
+ logCapture.setPrintLogs(printLogs);
+ if (listener != null) {
+ logCapture.addListener(listener);
+ }
+ installLogCapture();
+
+ System.out.println("Running simulation for " + maxTime + "ms...");
+
+ // Run simulation
+ Future<Void> runFuture = executor.submit(() -> {
+ try {
+ runSimulationSteps(maxTime);
+ } catch (Exception e) {
+ System.err.println("Error during simulation: " + e.getMessage());
+ e.printStackTrace();
+ }
+ return null;
+ });
+
+ // Wait for completion or timeout
+ try {
+ runFuture.get(maxTime * 2, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException e) {
+ System.out.println("Simulation timeout - stopping...");
+ runFuture.cancel(true);
+ }
+
+ System.out.println("Simulation complete. Captured " +
+ logCapture.getTotalLogCount() + " log entries.");
+
+ return new SimulationResult(
+ logCapture.getCapturedLogs(),
+ logCapture.getProcessLogs(),
+ getSimulationMetrics()
+ );
+ } catch (Exception e) {
+ System.err.println("Failed to load simulation: " + e.getMessage());
+ throw e;
+ }
+ }
+
+ private void runSimulationSteps(long maxTime) throws Exception {
+ VSTaskManager taskManager = viz.getTaskManager();
+
+ // Get necessary fields via reflection
+ Field timeField = VSSimulatorVisualization.class
+ .getDeclaredField("time");
+ timeField.setAccessible(true);
+
+ // Find runTasks method with correct signature
+ Method runTasksMethod = VSTaskManager.class
+ .getDeclaredMethod("runTasks", long.class, long.class, long.class);
+ runTasksMethod.setAccessible(true);
+
+ long startTime = timeField.getLong(viz);
+ long currentTime = startTime;
+
+ while (currentTime - startTime < maxTime) {
+ // Update time
+ timeField.setLong(viz, currentTime);
+
+ // Sync process times
+ for (int i = 0; i < viz.getNumProcesses(); i++) {
+ viz.getProcess(i).syncTime(currentTime);
+ }
+
+ // Run tasks (step, offset, lastGlobalTime)
+ runTasksMethod.invoke(taskManager, currentTime, 0L, currentTime - 1);
+
+ // Advance time by 1ms
+ currentTime++;
+
+ // Small delay to prevent CPU spinning
+ Thread.sleep(1);
+ }
+ }
+
+ private void installLogCapture() throws Exception {
+ // Set simulatorVisualization reference in logCapture
+ logCapture.setSimulatorCanvas(viz);
+
+ // Install on visualization
+ Field logingField = VSSimulatorVisualization.class
+ .getDeclaredField("loging");
+ logingField.setAccessible(true);
+ logingField.set(viz, logCapture);
+
+ // Install on all processes
+ for (int i = 0; i < viz.getNumProcesses(); i++) {
+ VSInternalProcess process = viz.getProcess(i);
+ if (process != null) {
+ Field processLogingField = VSAbstractProcess.class
+ .getDeclaredField("loging");
+ processLogingField.setAccessible(true);
+ processLogingField.set(process, logCapture);
+ }
+ }
+ }
+
+ private SimulationMetrics getSimulationMetrics() {
+ return new SimulationMetrics(
+ viz.getNumProcesses(),
+ logCapture.getTotalLogCount(),
+ logCapture.getProcessMessageCounts()
+ );
+ }
+
+ public void setPrintLogs(boolean printLogs) {
+ this.printLogs = printLogs;
+ if (logCapture != null) {
+ logCapture.setPrintLogs(printLogs);
+ }
+ }
+
+ public void addLogListener(LogListener listener) {
+ if (logCapture != null) {
+ logCapture.addListener(listener);
+ }
+ }
+
+ public void shutdown() {
+ executor.shutdown();
+ try {
+ if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+ executor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ executor.shutdownNow();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/LogCapture.java b/src/main/java/testing/LogCapture.java
new file mode 100644
index 0000000..59f7ede
--- /dev/null
+++ b/src/main/java/testing/LogCapture.java
@@ -0,0 +1,158 @@
+package testing;
+
+import simulator.VSLogging;
+import simulator.VSSimulatorVisualization;
+import core.VSInternalProcess;
+import java.util.*;
+import java.lang.reflect.Field;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Custom logging implementation that captures all log messages during
+ * headless simulation execution for later verification.
+ */
+public class LogCapture extends VSLogging {
+ private final List<LogEntry> capturedLogs;
+ private final Map<Integer, List<LogEntry>> processLogs;
+ private final List<LogListener> listeners;
+ private boolean printLogs = false;
+ private String logPrefix = "[LOG] ";
+
+ public LogCapture() {
+ super();
+ this.capturedLogs = new CopyOnWriteArrayList<>();
+ this.processLogs = new ConcurrentHashMap<>();
+ this.listeners = new CopyOnWriteArrayList<>();
+ }
+
+ public void setPrintLogs(boolean printLogs) {
+ this.printLogs = printLogs;
+ }
+
+ public void setLogPrefix(String prefix) {
+ this.logPrefix = prefix;
+ }
+
+ @Override
+ public synchronized void log(String message) {
+ // Call parent to maintain compatibility
+ super.log(message);
+
+ long time = 0;
+ if (getSimulatorVisualization() != null) {
+ time = getSimulatorVisualization().getTime();
+ }
+
+ LogEntry entry = new LogEntry(time, message, LogType.GLOBAL, -1);
+ capturedLogs.add(entry);
+ notifyListeners(entry);
+
+ if (printLogs) {
+ System.out.println(logPrefix + entry);
+ }
+ }
+
+ @Override
+ public synchronized void log(String message, long time) {
+ super.log(message, time);
+
+ LogEntry entry = new LogEntry(time, message, LogType.GLOBAL, -1);
+ capturedLogs.add(entry);
+ notifyListeners(entry);
+
+ if (printLogs) {
+ System.out.println(logPrefix + entry);
+ }
+ }
+
+ /**
+ * Log a message from a specific process.
+ * Note: This method is called by protocols and events.
+ */
+ public synchronized void log(VSInternalProcess process, String message) {
+ // Create formatted message for parent
+ String formattedMessage = "Process " + process.getProcessNum() +
+ ": " + message;
+ super.log(formattedMessage, process.getTime());
+
+ LogEntry entry = new LogEntry(
+ process.getTime(),
+ message,
+ LogType.PROCESS,
+ process.getProcessNum()
+ );
+
+ capturedLogs.add(entry);
+ processLogs.computeIfAbsent(process.getProcessNum(),
+ k -> new CopyOnWriteArrayList<>())
+ .add(entry);
+ notifyListeners(entry);
+
+ if (printLogs) {
+ System.out.println(logPrefix + "[P" + process.getProcessNum() + "] " + message);
+ }
+ }
+
+ private void notifyListeners(LogEntry entry) {
+ for (LogListener listener : listeners) {
+ try {
+ listener.onLogEntry(entry);
+ } catch (Exception e) {
+ System.err.println("Error notifying log listener: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Get the simulator visualization reference.
+ */
+ private VSSimulatorVisualization getSimulatorVisualization() {
+ try {
+ Field field = VSLogging.class.getDeclaredField("simulatorVisualization");
+ field.setAccessible(true);
+ return (VSSimulatorVisualization) field.get(this);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public List<LogEntry> getCapturedLogs() {
+ return new ArrayList<>(capturedLogs);
+ }
+
+ public Map<Integer, List<LogEntry>> getProcessLogs() {
+ Map<Integer, List<LogEntry>> result = new HashMap<>();
+ for (Map.Entry<Integer, List<LogEntry>> entry : processLogs.entrySet()) {
+ result.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+ }
+ return result;
+ }
+
+ public int getTotalLogCount() {
+ return capturedLogs.size();
+ }
+
+ public Map<Integer, Integer> getProcessMessageCounts() {
+ Map<Integer, Integer> counts = new HashMap<>();
+ for (Map.Entry<Integer, List<LogEntry>> entry : processLogs.entrySet()) {
+ counts.put(entry.getKey(), entry.getValue().size());
+ }
+ return counts;
+ }
+
+ public void addListener(LogListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeListener(LogListener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public synchronized void clear() {
+ super.clear();
+ capturedLogs.clear();
+ processLogs.clear();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/LogEntry.java b/src/main/java/testing/LogEntry.java
new file mode 100644
index 0000000..6bb2ac7
--- /dev/null
+++ b/src/main/java/testing/LogEntry.java
@@ -0,0 +1,73 @@
+package testing;
+
+/**
+ * Represents a single log entry captured during simulation execution.
+ * Immutable data class for thread-safe log collection.
+ */
+public class LogEntry {
+ private final long timestamp;
+ private final String message;
+ private final LogType type;
+ private final int processNum;
+
+ public LogEntry(long timestamp, String message, LogType type, int processNum) {
+ this.timestamp = timestamp;
+ this.message = message;
+ this.type = type;
+ this.processNum = processNum;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public LogType getType() {
+ return type;
+ }
+
+ public int getProcessNum() {
+ return processNum;
+ }
+
+ public boolean isFromProcess(int processNum) {
+ return this.processNum == processNum;
+ }
+
+ public boolean isGlobal() {
+ return type == LogType.GLOBAL;
+ }
+
+ @Override
+ public String toString() {
+ if (type == LogType.PROCESS) {
+ return String.format("[%d] Process %d: %s", timestamp, processNum, message);
+ } else {
+ return String.format("[%d] %s", timestamp, message);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ LogEntry logEntry = (LogEntry) o;
+ return timestamp == logEntry.timestamp &&
+ processNum == logEntry.processNum &&
+ type == logEntry.type &&
+ message.equals(logEntry.message);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Long.hashCode(timestamp);
+ result = 31 * result + message.hashCode();
+ result = 31 * result + type.hashCode();
+ result = 31 * result + processNum;
+ return result;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/LogListener.java b/src/main/java/testing/LogListener.java
new file mode 100644
index 0000000..e7dc350
--- /dev/null
+++ b/src/main/java/testing/LogListener.java
@@ -0,0 +1,14 @@
+package testing;
+
+/**
+ * Interface for receiving log events in real-time during simulation execution.
+ * Useful for monitoring, debugging, or implementing custom verification logic.
+ */
+public interface LogListener {
+ /**
+ * Called when a new log entry is captured.
+ *
+ * @param entry The captured log entry
+ */
+ void onLogEntry(LogEntry entry);
+} \ No newline at end of file
diff --git a/src/main/java/testing/LogType.java b/src/main/java/testing/LogType.java
new file mode 100644
index 0000000..c398304
--- /dev/null
+++ b/src/main/java/testing/LogType.java
@@ -0,0 +1,21 @@
+package testing;
+
+/**
+ * Enum representing the type of log entry.
+ */
+public enum LogType {
+ /**
+ * Global log message not associated with a specific process
+ */
+ GLOBAL,
+
+ /**
+ * Process-specific log message
+ */
+ PROCESS,
+
+ /**
+ * System-level message (errors, warnings)
+ */
+ SYSTEM
+} \ No newline at end of file
diff --git a/src/main/java/testing/ProtocolTestRunner.java b/src/main/java/testing/ProtocolTestRunner.java
new file mode 100644
index 0000000..f035325
--- /dev/null
+++ b/src/main/java/testing/ProtocolTestRunner.java
@@ -0,0 +1,220 @@
+package testing;
+
+import java.util.*;
+
+/**
+ * Runs all protocol tests and reports results.
+ * This is a standalone test runner that doesn't require JUnit.
+ */
+public class ProtocolTestRunner {
+
+ private static class TestCase {
+ final String name;
+ final String simulationFile;
+ final long duration;
+ final ProtocolVerifier verifier;
+
+ TestCase(String name, String simulationFile, long duration, ProtocolVerifier verifier) {
+ this.name = name;
+ this.simulationFile = simulationFile;
+ this.duration = duration;
+ this.verifier = verifier;
+ }
+ }
+
+ public static void main(String[] args) {
+ System.out.println("=== DS-Sim Protocol Test Runner ===\n");
+
+ // Check for verbose flag
+ boolean verbose = args.length > 0 &&
+ (args[0].equals("-v") || args[0].equals("--verbose"));
+
+ List<TestCase> tests = createTestCases();
+ int passed = 0;
+ int failed = 0;
+
+ HeadlessSimulationRunner runner = new HeadlessSimulationRunner();
+ runner.setPrintLogs(verbose);
+
+ for (TestCase test : tests) {
+ System.out.println("\n" + "=".repeat(60));
+ System.out.println("Testing " + test.name);
+ System.out.println("Simulation: " + test.simulationFile);
+ System.out.println("=".repeat(60));
+
+ try {
+ SimulationResult result = runner.runSimulation(
+ test.simulationFile,
+ test.duration
+ );
+
+ if (!verbose) {
+ System.out.println("\nCaptured " + result.getAllLogs().size() + " log entries");
+ }
+
+ VerificationResult verification = test.verifier.verify(result.getAllLogs());
+
+ if (verification.passed()) {
+ System.out.println("\n✓ PASSED");
+ passed++;
+ } else {
+ System.out.println("\n✗ FAILED");
+ System.out.println(" " + verification.getFailureMessage());
+ if (!verbose && result.getAllLogs().size() > 0) {
+ System.out.println("\n First few logs:");
+ result.getAllLogs().stream()
+ .limit(5)
+ .forEach(log -> System.out.println(" " + log));
+ }
+ failed++;
+ }
+
+ } catch (Exception e) {
+ System.out.println("\n✗ ERROR: " + e.getMessage());
+ if (verbose) {
+ e.printStackTrace();
+ }
+ failed++;
+ }
+ }
+
+ runner.shutdown();
+
+ System.out.println("\n" + "=".repeat(60));
+ System.out.println("=== Summary ===");
+ System.out.println("Total tests: " + tests.size());
+ System.out.println("Passed: " + passed);
+ System.out.println("Failed: " + failed);
+
+ if (failed == 0) {
+ System.out.println("\n✓ All tests passed!");
+ System.exit(0);
+ } else {
+ System.out.println("\n✗ Some tests failed!");
+ System.out.println("\nRun with -v or --verbose to see detailed logs");
+ System.exit(1);
+ }
+ }
+
+ private static List<TestCase> createTestCases() {
+ List<TestCase> tests = new ArrayList<>();
+
+ // Ping-Pong
+ tests.add(new TestCase(
+ "Ping-Pong",
+ "saved-simulations/ping-pong.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("Ping-Pong.*activated")
+ .expectLog("Message sent")
+ .expectLog("Message received")
+ .expectNoLog("ERROR")
+ ));
+
+ // Ping-Pong Sturm
+ tests.add(new TestCase(
+ "Ping-Pong Sturm",
+ "saved-simulations/ping-pong-sturm.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("Ping-Pong.*activated")
+ .expectLog("Message")
+ .expectNoLog("ERROR")
+ ));
+
+ // Broadcast
+ tests.add(new TestCase(
+ "Broadcast",
+ "saved-simulations/broadcast.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("Broadcast.*activated")
+ .expectLog("Message")
+ .expectNoLog("ERROR")
+ ));
+
+ // Basic Multicast
+ tests.add(new TestCase(
+ "Basic Multicast",
+ "saved-simulations/basic-multicast.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("Basic Multicast.*activated|Multicast.*activated")
+ .expectLog("Message")
+ .expectNoLog("ERROR")
+ ));
+
+ // Reliable Multicast
+ tests.add(new TestCase(
+ "Reliable Multicast",
+ "saved-simulations/reliable-multicast.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("Reliable Multicast.*activated")
+ .expectLog("Message")
+ .expectNoLog("ERROR")
+ ));
+
+ // Berkeley Time Sync
+ tests.add(new TestCase(
+ "Berkeley Time Sync",
+ "saved-simulations/berkeley.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("Berkley.*activated|Berkeley.*activated")
+ .expectNoLog("ERROR")
+ ));
+
+ // Internal Time Sync
+ tests.add(new TestCase(
+ "Internal Time Sync",
+ "saved-simulations/int-sync.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("Internal.*sync.*activated")
+ .expectNoLog("ERROR")
+ ));
+
+ // External vs Internal Sync
+ tests.add(new TestCase(
+ "External vs Internal Sync",
+ "saved-simulations/ext-vs-int-sync.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("activated")
+ .expectNoLog("ERROR")
+ ));
+
+ // One-Phase Commit
+ tests.add(new TestCase(
+ "One-Phase Commit",
+ "saved-simulations/one-phase-commit.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("1-Phase Commit.*activated")
+ .expectNoLog("ERROR")
+ ));
+
+ // Two-Phase Commit
+ tests.add(new TestCase(
+ "Two-Phase Commit",
+ "saved-simulations/two-phase-commit.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("2-Phase Commit.*activated")
+ .expectNoLog("ERROR")
+ ));
+
+ // Slow Connection
+ tests.add(new TestCase(
+ "Slow Connection",
+ "saved-simulations/slow-connection.dat",
+ 2000,
+ new ProtocolVerifier()
+ .expectLog("activated")
+ .expectNoLog("ERROR")
+ ));
+
+ return tests;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/ProtocolTestRunnerWithLogs.java b/src/main/java/testing/ProtocolTestRunnerWithLogs.java
new file mode 100644
index 0000000..fc6e167
--- /dev/null
+++ b/src/main/java/testing/ProtocolTestRunnerWithLogs.java
@@ -0,0 +1,114 @@
+package testing;
+
+import java.util.*;
+
+/**
+ * Protocol test runner that shows logs during execution for better visibility.
+ */
+public class ProtocolTestRunnerWithLogs {
+
+ public static void main(String[] args) {
+ System.out.println("=== DS-Sim Protocol Test Runner (with logs) ===\n");
+
+ // Simple test configuration
+ String[][] tests = {
+ {"Ping-Pong", "saved-simulations/ping-pong.dat"},
+ {"Broadcast", "saved-simulations/broadcast.dat"},
+ {"Basic Multicast", "saved-simulations/basic-multicast.dat"},
+ {"Berkeley Time Sync", "saved-simulations/berkeley.dat"},
+ {"One-Phase Commit", "saved-simulations/one-phase-commit.dat"},
+ {"Two-Phase Commit", "saved-simulations/two-phase-commit.dat"}
+ };
+
+ int passed = 0;
+ int failed = 0;
+
+ for (String[] test : tests) {
+ String name = test[0];
+ String file = test[1];
+
+ System.out.println("\n" + "=".repeat(70));
+ System.out.println("TEST: " + name);
+ System.out.println("FILE: " + file);
+ System.out.println("=".repeat(70));
+
+ HeadlessSimulationRunner runner = new HeadlessSimulationRunner();
+
+ try {
+ // Create a custom log listener to format output nicely
+ final int[] logCount = {0};
+ final int maxLogs = 20; // Show first 20 logs
+
+ LogListener listener = new LogListener() {
+ @Override
+ public void onLogEntry(LogEntry entry) {
+ if (logCount[0]++ < maxLogs) {
+ String timestamp = String.format("[%4dms]", entry.getTimestamp());
+ String process = entry.getType() == LogType.PROCESS ?
+ "P" + entry.getProcessNum() : "SYS";
+ System.out.printf("%s %3s: %s\n",
+ timestamp, process, entry.getMessage());
+ } else if (logCount[0] == maxLogs) {
+ System.out.println("... (more logs hidden)");
+ }
+ }
+ };
+
+ // Run simulation with listener
+ System.out.println("\nRunning simulation for 2 seconds...\n");
+ SimulationResult result = runner.runSimulation(file, 2000, listener);
+
+ // If no logs were printed in real-time, show them now
+ if (logCount[0] == 0 && result.getAllLogs().size() > 0) {
+ System.out.println("Captured logs:");
+ result.getAllLogs().stream()
+ .limit(maxLogs)
+ .forEach(log -> {
+ String timestamp = String.format("[%4dms]", log.getTimestamp());
+ String process = log.getType() == LogType.PROCESS ?
+ "P" + log.getProcessNum() : "SYS";
+ System.out.printf("%s %3s: %s\n",
+ timestamp, process, log.getMessage());
+ });
+ }
+
+ // Simple verification
+ boolean hasActivation = result.countLogs("activated") > 0;
+ boolean hasMessages = result.countLogs("Message") > 0;
+ boolean hasErrors = result.countLogs("ERROR") > 0 ||
+ result.countLogs("Exception") > 0;
+
+ System.out.println("\nVerification:");
+ System.out.println(" Protocol activated: " + (hasActivation ? "✓" : "✗"));
+ System.out.println(" Messages exchanged: " + (hasMessages ? "✓" : "✗"));
+ System.out.println(" No errors: " + (!hasErrors ? "✓" : "✗"));
+ System.out.println(" Total logs: " + result.getAllLogs().size());
+
+ if (hasActivation && !hasErrors) {
+ System.out.println("\n✓ PASSED");
+ passed++;
+ } else {
+ System.out.println("\n✗ FAILED");
+ failed++;
+ }
+
+ } catch (Exception e) {
+ System.out.println("\n✗ ERROR: " + e.getMessage());
+ failed++;
+ } finally {
+ runner.shutdown();
+ }
+ }
+
+ // Summary
+ System.out.println("\n" + "=".repeat(70));
+ System.out.println("SUMMARY");
+ System.out.println("=".repeat(70));
+ System.out.println("Total tests: " + tests.length);
+ System.out.println("Passed: " + passed);
+ System.out.println("Failed: " + failed);
+ System.out.println();
+
+ System.exit(failed == 0 ? 0 : 1);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/ProtocolVerifier.java b/src/main/java/testing/ProtocolVerifier.java
new file mode 100644
index 0000000..19ed1f2
--- /dev/null
+++ b/src/main/java/testing/ProtocolVerifier.java
@@ -0,0 +1,243 @@
+package testing;
+
+import java.util.*;
+import java.util.regex.*;
+import java.util.function.Predicate;
+
+/**
+ * Flexible verification system for checking protocol behavior through log analysis.
+ * Supports pattern matching, sequence verification, and count-based assertions.
+ */
+public class ProtocolVerifier {
+ private final List<VerificationRule> rules;
+
+ public ProtocolVerifier() {
+ this.rules = new ArrayList<>();
+ }
+
+ /**
+ * Add a custom verification rule.
+ */
+ public ProtocolVerifier withRule(VerificationRule rule) {
+ rules.add(rule);
+ return this;
+ }
+
+ /**
+ * Expect a log message containing the pattern at least once.
+ */
+ public ProtocolVerifier expectLog(String pattern) {
+ rules.add(new PatternRule(pattern, 1, Integer.MAX_VALUE));
+ return this;
+ }
+
+ /**
+ * Expect a log message containing the pattern exactly n times.
+ */
+ public ProtocolVerifier expectLogExactly(String pattern, int count) {
+ rules.add(new PatternRule(pattern, count, count));
+ return this;
+ }
+
+ /**
+ * Expect a log message containing the pattern at least n times.
+ */
+ public ProtocolVerifier expectLogAtLeast(String pattern, int minCount) {
+ rules.add(new PatternRule(pattern, minCount, Integer.MAX_VALUE));
+ return this;
+ }
+
+ /**
+ * Expect a log message containing the pattern at most n times.
+ */
+ public ProtocolVerifier expectLogAtMost(String pattern, int maxCount) {
+ rules.add(new PatternRule(pattern, 0, maxCount));
+ return this;
+ }
+
+ /**
+ * Expect a sequence of patterns in order.
+ */
+ public ProtocolVerifier expectSequence(String... patterns) {
+ rules.add(new SequenceRule(Arrays.asList(patterns)));
+ return this;
+ }
+
+ /**
+ * Expect no log messages containing the pattern.
+ */
+ public ProtocolVerifier expectNoLog(String pattern) {
+ rules.add(new PatternRule(pattern, 0, 0));
+ return this;
+ }
+
+ /**
+ * Expect a log from a specific process.
+ */
+ public ProtocolVerifier expectLogFromProcess(int processNum, String pattern) {
+ rules.add(new ProcessPatternRule(processNum, pattern, 1, Integer.MAX_VALUE));
+ return this;
+ }
+
+ /**
+ * Verify all rules against the provided logs.
+ */
+ public VerificationResult verify(List<LogEntry> logs) {
+ List<RuleResult> results = new ArrayList<>();
+
+ for (VerificationRule rule : rules) {
+ results.add(rule.verify(logs));
+ }
+
+ return new VerificationResult(results);
+ }
+
+ // Rule implementations
+
+ /**
+ * Rule that matches log messages against a pattern.
+ */
+ private static class PatternRule implements VerificationRule {
+ private final Pattern pattern;
+ private final int minCount;
+ private final int maxCount;
+ private final String description;
+
+ public PatternRule(String pattern, int minCount, int maxCount) {
+ // Try to compile as regex first, if it fails, use literal matching
+ Pattern compiledPattern;
+ try {
+ compiledPattern = Pattern.compile(pattern);
+ } catch (PatternSyntaxException e) {
+ // If not a valid regex, escape it for literal matching
+ compiledPattern = Pattern.compile(Pattern.quote(pattern));
+ }
+ this.pattern = compiledPattern;
+ this.minCount = minCount;
+ this.maxCount = maxCount;
+ this.description = String.format(
+ "Pattern '%s' should appear %s times",
+ pattern,
+ minCount == maxCount ?
+ String.valueOf(minCount) :
+ minCount + "-" + (maxCount == Integer.MAX_VALUE ? "∞" : maxCount)
+ );
+ }
+
+ @Override
+ public RuleResult verify(List<LogEntry> logs) {
+ int count = 0;
+ List<LogEntry> matches = new ArrayList<>();
+
+ for (LogEntry log : logs) {
+ if (pattern.matcher(log.getMessage()).find()) {
+ count++;
+ matches.add(log);
+ }
+ }
+
+ boolean passed = count >= minCount && count <= maxCount;
+ String message = String.format(
+ "%s (found %d occurrences)",
+ description, count
+ );
+
+ return new RuleResult(passed, message, matches);
+ }
+ }
+
+ /**
+ * Rule that verifies a sequence of patterns appear in order.
+ */
+ private static class SequenceRule implements VerificationRule {
+ private final List<Pattern> patterns;
+ private final String description;
+
+ public SequenceRule(List<String> patterns) {
+ this.patterns = new ArrayList<>();
+ for (String p : patterns) {
+ try {
+ this.patterns.add(Pattern.compile(p));
+ } catch (PatternSyntaxException e) {
+ this.patterns.add(Pattern.compile(Pattern.quote(p)));
+ }
+ }
+ this.description = "Sequence: " + String.join(" → ", patterns);
+ }
+
+ @Override
+ public RuleResult verify(List<LogEntry> logs) {
+ int patternIndex = 0;
+ List<LogEntry> matches = new ArrayList<>();
+
+ for (LogEntry log : logs) {
+ if (patternIndex < patterns.size() &&
+ patterns.get(patternIndex).matcher(log.getMessage()).find()) {
+ matches.add(log);
+ patternIndex++;
+ }
+ }
+
+ boolean passed = patternIndex == patterns.size();
+ String message = String.format(
+ "%s (%d/%d patterns matched)",
+ description, patternIndex, patterns.size()
+ );
+
+ return new RuleResult(passed, message, matches);
+ }
+ }
+
+ /**
+ * Rule that matches patterns from a specific process.
+ */
+ private static class ProcessPatternRule implements VerificationRule {
+ private final int processNum;
+ private final Pattern pattern;
+ private final int minCount;
+ private final int maxCount;
+ private final String description;
+
+ public ProcessPatternRule(int processNum, String pattern, int minCount, int maxCount) {
+ this.processNum = processNum;
+ Pattern compiledPattern;
+ try {
+ compiledPattern = Pattern.compile(pattern);
+ } catch (PatternSyntaxException e) {
+ compiledPattern = Pattern.compile(Pattern.quote(pattern));
+ }
+ this.pattern = compiledPattern;
+ this.minCount = minCount;
+ this.maxCount = maxCount;
+ this.description = String.format(
+ "Process %d: Pattern '%s' should appear %s times",
+ processNum, pattern,
+ minCount == maxCount ?
+ String.valueOf(minCount) :
+ minCount + "-" + (maxCount == Integer.MAX_VALUE ? "∞" : maxCount)
+ );
+ }
+
+ @Override
+ public RuleResult verify(List<LogEntry> logs) {
+ int count = 0;
+ List<LogEntry> matches = new ArrayList<>();
+
+ for (LogEntry log : logs) {
+ if (log.isFromProcess(processNum) &&
+ pattern.matcher(log.getMessage()).find()) {
+ count++;
+ matches.add(log);
+ }
+ }
+
+ boolean passed = count >= minCount && count <= maxCount;
+ String message = String.format(
+ "%s (found %d occurrences)",
+ description, count
+ );
+
+ return new RuleResult(passed, message, matches);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/QuietProtocolTestRunner.java b/src/main/java/testing/QuietProtocolTestRunner.java
new file mode 100644
index 0000000..d3b35a1
--- /dev/null
+++ b/src/main/java/testing/QuietProtocolTestRunner.java
@@ -0,0 +1,79 @@
+package testing;
+
+import java.io.PrintStream;
+import java.io.OutputStream;
+
+/**
+ * A test runner that suppresses GUI-related error messages while still showing test results.
+ * This provides a cleaner output when running headless tests.
+ */
+public class QuietProtocolTestRunner {
+
+ public static void main(String[] args) {
+ // Create a custom PrintStream that filters out specific error messages
+ PrintStream originalErr = System.err;
+ PrintStream filteredErr = new PrintStream(new FilteredOutputStream(originalErr));
+
+ try {
+ // Redirect System.err to our filtered stream
+ System.setErr(filteredErr);
+
+ // Run the actual test runner
+ System.out.println("=== DS-Sim Protocol Test Runner (Quiet Mode) ===\n");
+ System.out.println("Note: GUI errors are suppressed for cleaner output.\n");
+
+ // Pass through any arguments (like -v for verbose)
+ ProtocolTestRunnerWithLogs.main(args);
+
+ } finally {
+ // Restore original error stream
+ System.setErr(originalErr);
+ }
+ }
+
+ /**
+ * An OutputStream that filters out specific error messages.
+ */
+ private static class FilteredOutputStream extends OutputStream {
+ private final PrintStream target;
+ private final StringBuilder buffer = new StringBuilder();
+
+ public FilteredOutputStream(PrintStream target) {
+ this.target = target;
+ }
+
+ @Override
+ public void write(int b) {
+ buffer.append((char) b);
+
+ // Check if we have a complete line
+ if (b == '\n') {
+ String line = buffer.toString();
+
+ // Filter out specific GUI-related errors
+ if (!line.contains("Component must have a valid peer") &&
+ !line.contains("java.lang.IllegalStateException") &&
+ !line.contains("at java.desktop/") &&
+ !line.contains("at simulator.VSSimulatorVisualization.paint") &&
+ !line.contains("createBufferStrategy") &&
+ !line.contains("FlipBufferStrategy")) {
+
+ // Pass through other messages
+ target.print(line);
+ }
+
+ buffer.setLength(0);
+ }
+ }
+
+ @Override
+ public void flush() {
+ target.flush();
+ }
+
+ @Override
+ public void close() {
+ target.close();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/RuleResult.java b/src/main/java/testing/RuleResult.java
new file mode 100644
index 0000000..4414f34
--- /dev/null
+++ b/src/main/java/testing/RuleResult.java
@@ -0,0 +1,40 @@
+package testing;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Result of applying a verification rule to simulation logs.
+ */
+public class RuleResult {
+ private final boolean passed;
+ private final String message;
+ private final List<LogEntry> relevantLogs;
+
+ public RuleResult(boolean passed, String message, List<LogEntry> relevantLogs) {
+ this.passed = passed;
+ this.message = message;
+ this.relevantLogs = Collections.unmodifiableList(relevantLogs);
+ }
+
+ public RuleResult(boolean passed, String message) {
+ this(passed, message, Collections.emptyList());
+ }
+
+ public boolean isPassed() {
+ return passed;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public List<LogEntry> getRelevantLogs() {
+ return relevantLogs;
+ }
+
+ @Override
+ public String toString() {
+ return (passed ? "PASS" : "FAIL") + ": " + message;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/SimulationMetrics.java b/src/main/java/testing/SimulationMetrics.java
new file mode 100644
index 0000000..2b80631
--- /dev/null
+++ b/src/main/java/testing/SimulationMetrics.java
@@ -0,0 +1,47 @@
+package testing;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Metrics collected during simulation execution.
+ */
+public class SimulationMetrics {
+ private final int numProcesses;
+ private final int totalLogCount;
+ private final Map<Integer, Integer> processMessageCounts;
+
+ public SimulationMetrics(int numProcesses,
+ int totalLogCount,
+ Map<Integer, Integer> processMessageCounts) {
+ this.numProcesses = numProcesses;
+ this.totalLogCount = totalLogCount;
+ this.processMessageCounts = Collections.unmodifiableMap(processMessageCounts);
+ }
+
+ public int getNumProcesses() {
+ return numProcesses;
+ }
+
+ public int getTotalLogCount() {
+ return totalLogCount;
+ }
+
+ public Map<Integer, Integer> getProcessMessageCounts() {
+ return processMessageCounts;
+ }
+
+ public int getMessageCountForProcess(int processNum) {
+ return processMessageCounts.getOrDefault(processNum, 0);
+ }
+
+ public double getAverageMessagesPerProcess() {
+ if (numProcesses == 0) return 0;
+
+ int totalProcessMessages = processMessageCounts.values().stream()
+ .mapToInt(Integer::intValue)
+ .sum();
+
+ return (double) totalProcessMessages / numProcesses;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/SimulationResult.java b/src/main/java/testing/SimulationResult.java
new file mode 100644
index 0000000..4cab8ee
--- /dev/null
+++ b/src/main/java/testing/SimulationResult.java
@@ -0,0 +1,94 @@
+package testing;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Contains the results of a headless simulation run, including
+ * captured logs and execution metrics.
+ */
+public class SimulationResult {
+ private final List<LogEntry> allLogs;
+ private final Map<Integer, List<LogEntry>> processLogs;
+ private final SimulationMetrics metrics;
+
+ public SimulationResult(List<LogEntry> allLogs,
+ Map<Integer, List<LogEntry>> processLogs,
+ SimulationMetrics metrics) {
+ this.allLogs = Collections.unmodifiableList(allLogs);
+ this.processLogs = Collections.unmodifiableMap(processLogs);
+ this.metrics = metrics;
+ }
+
+ public List<LogEntry> getAllLogs() {
+ return allLogs;
+ }
+
+ public List<LogEntry> getLogsForProcess(int processNum) {
+ return processLogs.getOrDefault(processNum, Collections.emptyList());
+ }
+
+ public Map<Integer, List<LogEntry>> getProcessLogs() {
+ return processLogs;
+ }
+
+ public SimulationMetrics getMetrics() {
+ return metrics;
+ }
+
+ /**
+ * Count logs matching a pattern
+ */
+ public int countLogs(String pattern) {
+ return (int) allLogs.stream()
+ .filter(log -> log.getMessage().contains(pattern))
+ .count();
+ }
+
+ /**
+ * Find first log matching pattern
+ */
+ public Optional<LogEntry> findFirst(String pattern) {
+ return allLogs.stream()
+ .filter(log -> log.getMessage().contains(pattern))
+ .findFirst();
+ }
+
+ /**
+ * Find all logs matching pattern
+ */
+ public List<LogEntry> findAll(String pattern) {
+ return allLogs.stream()
+ .filter(log -> log.getMessage().contains(pattern))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Get logs in time range
+ */
+ public List<LogEntry> getLogsInTimeRange(long startTime, long endTime) {
+ return allLogs.stream()
+ .filter(log -> log.getTimestamp() >= startTime &&
+ log.getTimestamp() <= endTime)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Generate summary report
+ */
+ public String generateSummary() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("=== Simulation Result Summary ===\n");
+ sb.append("Total logs: ").append(allLogs.size()).append("\n");
+ sb.append("Processes: ").append(metrics.getNumProcesses()).append("\n");
+ sb.append("\nLogs per process:\n");
+
+ for (Map.Entry<Integer, Integer> entry :
+ metrics.getProcessMessageCounts().entrySet()) {
+ sb.append(" Process ").append(entry.getKey())
+ .append(": ").append(entry.getValue()).append(" logs\n");
+ }
+
+ return sb.toString();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/VerificationResult.java b/src/main/java/testing/VerificationResult.java
new file mode 100644
index 0000000..3678ea8
--- /dev/null
+++ b/src/main/java/testing/VerificationResult.java
@@ -0,0 +1,57 @@
+package testing;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Aggregated result of all verification rules applied to a simulation.
+ */
+public class VerificationResult {
+ private final List<RuleResult> ruleResults;
+ private final boolean allPassed;
+
+ public VerificationResult(List<RuleResult> ruleResults) {
+ this.ruleResults = Collections.unmodifiableList(ruleResults);
+ this.allPassed = ruleResults.stream().allMatch(RuleResult::isPassed);
+ }
+
+ public boolean passed() {
+ return allPassed;
+ }
+
+ public List<RuleResult> getRuleResults() {
+ return ruleResults;
+ }
+
+ public List<RuleResult> getFailedRules() {
+ return ruleResults.stream()
+ .filter(r -> !r.isPassed())
+ .collect(Collectors.toList());
+ }
+
+ public String getFailureMessage() {
+ if (allPassed) {
+ return "All verification rules passed";
+ }
+
+ return "Failed rules:\n" + getFailedRules().stream()
+ .map(r -> " - " + r.getMessage())
+ .collect(Collectors.joining("\n"));
+ }
+
+ public String generateReport() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("=== Verification Report ===\n");
+ sb.append("Total rules: ").append(ruleResults.size()).append("\n");
+ sb.append("Passed: ").append(ruleResults.size() - getFailedRules().size()).append("\n");
+ sb.append("Failed: ").append(getFailedRules().size()).append("\n\n");
+
+ sb.append("Results:\n");
+ for (RuleResult result : ruleResults) {
+ sb.append(" ").append(result).append("\n");
+ }
+
+ return sb.toString();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/VerificationRule.java b/src/main/java/testing/VerificationRule.java
new file mode 100644
index 0000000..43d3933
--- /dev/null
+++ b/src/main/java/testing/VerificationRule.java
@@ -0,0 +1,26 @@
+package testing;
+
+import java.util.List;
+
+/**
+ * Interface for defining verification rules that can be applied
+ * to simulation logs to verify protocol behavior.
+ */
+public interface VerificationRule {
+ /**
+ * Verify the rule against the provided logs.
+ *
+ * @param logs The complete list of log entries from the simulation
+ * @return Result indicating whether the rule passed and details
+ */
+ RuleResult verify(List<LogEntry> logs);
+
+ /**
+ * Get a human-readable description of what this rule verifies.
+ *
+ * @return Description of the rule
+ */
+ default String getDescription() {
+ return this.getClass().getSimpleName();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/examples/InteractiveTest.java b/src/main/java/testing/examples/InteractiveTest.java
new file mode 100644
index 0000000..8cc93e8
--- /dev/null
+++ b/src/main/java/testing/examples/InteractiveTest.java
@@ -0,0 +1,66 @@
+package testing.examples;
+
+import testing.*;
+import java.util.Scanner;
+
+public class InteractiveTest {
+ public static void main(String[] args) throws Exception {
+ Scanner scanner = new Scanner(System.in);
+ HeadlessSimulationRunner runner = new HeadlessSimulationRunner();
+
+ System.out.println("=== Interactive Headless Test ===");
+ System.out.println("\nAvailable simulations:");
+ System.out.println("1. ping-pong.dat");
+ System.out.println("2. broadcast.dat");
+ System.out.println("3. berkeley.dat");
+ System.out.println("4. raft-working.dat");
+
+ System.out.print("\nEnter simulation filename (or full path): ");
+ String filename = scanner.nextLine();
+
+ // Add saved-simulations/ prefix if not present
+ if (!filename.contains("/")) {
+ filename = "saved-simulations/" + filename;
+ }
+
+ System.out.print("Run duration in ms (default 2000): ");
+ String durationStr = scanner.nextLine();
+ long duration = durationStr.isEmpty() ? 2000 : Long.parseLong(durationStr);
+
+ System.out.print("Pattern to search for (optional): ");
+ String pattern = scanner.nextLine();
+
+ try {
+ System.out.println("\nRunning simulation...");
+ SimulationResult result = runner.runSimulation(filename, duration);
+
+ System.out.println("\nResults:");
+ System.out.println("- Total logs: " + result.getAllLogs().size());
+ System.out.println("- Processes: " + result.getMetrics().getNumProcesses());
+
+ if (!pattern.isEmpty()) {
+ int count = result.countLogs(pattern);
+ System.out.println("- Pattern '" + pattern + "' found: " + count + " times");
+
+ if (count > 0) {
+ System.out.println("\nMatching logs:");
+ result.findAll(pattern).stream()
+ .limit(5)
+ .forEach(log -> System.out.println(" " + log));
+ }
+ }
+
+ System.out.println("\nFirst 10 logs:");
+ result.getAllLogs().stream()
+ .limit(10)
+ .forEach(log -> System.out.println(" [" + log.getTimestamp() + "] " +
+ log.getMessage()));
+
+ } catch (Exception e) {
+ System.err.println("Error: " + e.getMessage());
+ } finally {
+ runner.shutdown();
+ scanner.close();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/examples/QuickTest.java b/src/main/java/testing/examples/QuickTest.java
new file mode 100644
index 0000000..f6f3e86
--- /dev/null
+++ b/src/main/java/testing/examples/QuickTest.java
@@ -0,0 +1,40 @@
+package testing.examples;
+
+import testing.*;
+
+public class QuickTest {
+ public static void main(String[] args) throws Exception {
+ // Use command line arg or default
+ String simulationFile = args.length > 0 ? args[0] : "saved-simulations/ping-pong.dat";
+ long duration = args.length > 1 ? Long.parseLong(args[1]) : 1000;
+
+ if (args.length == 0) {
+ System.out.println("=== Quick Headless Test ===\n");
+ }
+
+ HeadlessSimulationRunner runner = new HeadlessSimulationRunner();
+
+ try {
+ SimulationResult result = runner.runSimulation(
+ simulationFile,
+ duration
+ );
+
+ System.out.println("Captured " + result.getAllLogs().size() + " logs");
+ System.out.println("\nFirst 5 logs:");
+ result.getAllLogs().stream()
+ .limit(5)
+ .forEach(log -> System.out.println(" " + log));
+
+ // Simple verification
+ boolean hasActivation = result.countLogs("activated") > 0;
+ boolean hasMessages = result.countLogs("Message") > 0;
+
+ System.out.println("\n✓ Protocol activated: " + hasActivation);
+ System.out.println("✓ Messages exchanged: " + hasMessages);
+
+ } finally {
+ runner.shutdown();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/examples/TestPingPongSimulation.java b/src/main/java/testing/examples/TestPingPongSimulation.java
new file mode 100644
index 0000000..9f16a27
--- /dev/null
+++ b/src/main/java/testing/examples/TestPingPongSimulation.java
@@ -0,0 +1,138 @@
+package testing.examples;
+
+import testing.*;
+import java.util.List;
+
+/**
+ * Test program to verify the headless testing framework with ping-pong simulation.
+ * This demonstrates how to use the framework to verify protocol behavior.
+ */
+public class TestPingPongSimulation {
+
+ public static void main(String[] args) {
+ System.out.println("=== Testing Ping-Pong Protocol ===\n");
+
+ HeadlessSimulationRunner runner = new HeadlessSimulationRunner();
+
+ try {
+ // Run the ping-pong simulation for 2 seconds
+ SimulationResult result = runner.runSimulation(
+ "saved-simulations/ping-pong.dat",
+ 2000
+ );
+
+ // Print summary
+ System.out.println("\n" + result.generateSummary());
+
+ // Show first 20 logs
+ System.out.println("\nFirst 20 log entries:");
+ List<LogEntry> logs = result.getAllLogs();
+ for (int i = 0; i < Math.min(20, logs.size()); i++) {
+ System.out.println(" " + logs.get(i));
+ }
+
+ // Verify ping-pong behavior
+ System.out.println("\n=== Verification ===");
+
+ ProtocolVerifier verifier = new ProtocolVerifier()
+ // Expect protocol activation
+ .expectLog("Ping-Pong.*activated")
+ // Expect ping messages
+ .expectLog("ping")
+ // Expect pong responses
+ .expectLog("pong")
+ // Expect alternating sequence
+ .expectSequence("ping", "pong")
+ // No errors expected
+ .expectNoLog("ERROR")
+ .expectNoLog("Exception");
+
+ VerificationResult verification = verifier.verify(result.getAllLogs());
+
+ System.out.println("\n" + verification.generateReport());
+
+ if (verification.passed()) {
+ System.out.println("\n✓ All verification rules passed!");
+ } else {
+ System.out.println("\n✗ Some verification rules failed:");
+ System.out.println(verification.getFailureMessage());
+ }
+
+ // Additional analysis
+ System.out.println("\n=== Protocol Analysis ===");
+
+ // Count ping and pong messages
+ int pingCount = result.countLogs("ping");
+ int pongCount = result.countLogs("pong");
+
+ System.out.println("Ping messages: " + pingCount);
+ System.out.println("Pong messages: " + pongCount);
+
+ // Check balance
+ if (Math.abs(pingCount - pongCount) <= 1) {
+ System.out.println("✓ Ping/Pong messages are balanced");
+ } else {
+ System.out.println("✗ Ping/Pong imbalance detected");
+ }
+
+ // Check for message patterns by process
+ System.out.println("\n=== Per-Process Analysis ===");
+ for (int i = 0; i < result.getMetrics().getNumProcesses(); i++) {
+ List<LogEntry> processLogs = result.getLogsForProcess(i);
+ if (!processLogs.isEmpty()) {
+ System.out.println("Process " + i + ":");
+ System.out.println(" Total messages: " + processLogs.size());
+
+ long pings = processLogs.stream()
+ .filter(log -> log.getMessage().contains("ping"))
+ .count();
+ long pongs = processLogs.stream()
+ .filter(log -> log.getMessage().contains("pong"))
+ .count();
+
+ System.out.println(" Pings sent: " + pings);
+ System.out.println(" Pongs sent: " + pongs);
+ }
+ }
+
+ // Test real-time monitoring
+ System.out.println("\n=== Testing Real-time Monitoring ===");
+
+ HeadlessSimulationRunner runner2 = new HeadlessSimulationRunner();
+
+ // Add a listener that prints ping/pong messages in real-time
+ class PingPongMonitor implements LogListener {
+ private int pingCount = 0;
+ private int pongCount = 0;
+
+ @Override
+ public void onLogEntry(LogEntry entry) {
+ if (entry.getMessage().contains("ping")) {
+ pingCount++;
+ if (pingCount <= 5) {
+ System.out.println(" [MONITOR] Ping #" + pingCount +
+ " at time " + entry.getTimestamp());
+ }
+ } else if (entry.getMessage().contains("pong")) {
+ pongCount++;
+ if (pongCount <= 5) {
+ System.out.println(" [MONITOR] Pong #" + pongCount +
+ " at time " + entry.getTimestamp());
+ }
+ }
+ }
+ }
+
+ // Note: We'd need to modify HeadlessSimulationRunner to expose
+ // the LogCapture to add listeners, but this shows the concept
+
+ System.out.println("\n=== Test Complete ===");
+
+ } catch (Exception e) {
+ System.err.println("Test failed with error: " + e.getMessage());
+ e.printStackTrace();
+ } finally {
+ runner.shutdown();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/testing/examples/TestPingPongVerified.java b/src/main/java/testing/examples/TestPingPongVerified.java
new file mode 100644
index 0000000..1a41a6a
--- /dev/null
+++ b/src/main/java/testing/examples/TestPingPongVerified.java
@@ -0,0 +1,132 @@
+package testing.examples;
+
+import testing.*;
+import java.util.List;
+
+/**
+ * Verified test program for ping-pong simulation that checks for actual logged messages.
+ */
+public class TestPingPongVerified {
+
+ public static void main(String[] args) {
+ System.out.println("=== Testing Ping-Pong Protocol (Verified) ===\n");
+
+ HeadlessSimulationRunner runner = new HeadlessSimulationRunner();
+
+ try {
+ // Run the ping-pong simulation for 3 seconds
+ SimulationResult result = runner.runSimulation(
+ "saved-simulations/ping-pong.dat",
+ 3000
+ );
+
+ // Print summary
+ System.out.println("\n" + result.generateSummary());
+
+ // Show all captured logs
+ System.out.println("\nAll captured log entries:");
+ List<LogEntry> logs = result.getAllLogs();
+ for (int i = 0; i < Math.min(30, logs.size()); i++) {
+ LogEntry log = logs.get(i);
+ System.out.printf("[%4d] %s %s\n",
+ log.getTimestamp(),
+ log.getType() == LogType.PROCESS ? "P" + log.getProcessNum() : "G",
+ log.getMessage());
+ }
+ if (logs.size() > 30) {
+ System.out.println("... (" + (logs.size() - 30) + " more entries)");
+ }
+
+ // Verify ping-pong behavior with correct patterns
+ System.out.println("\n=== Verification ===");
+
+ ProtocolVerifier verifier = new ProtocolVerifier()
+ // Expect protocol activation
+ .expectLogExactly("Ping-Pong.*activated", 2)
+ // Expect client activation first
+ .expectLog("Ping-Pong Client activated")
+ // Expect server activation
+ .expectLog("Ping-Pong Server activated")
+ // Expect message exchanges
+ .expectLog("Message sent")
+ .expectLog("Message received")
+ // Expect fromClient messages
+ .expectLog("fromClient=true")
+ // Expect fromServer messages
+ .expectLog("fromServer=true")
+ // Expect alternating pattern
+ .expectSequence("fromClient=true", "fromServer=true")
+ // Check counter increments
+ .expectLog("counter=1")
+ .expectLog("counter=2")
+ // No errors expected
+ .expectNoLog("ERROR")
+ .expectNoLog("Exception")
+ .expectNoLog("crashed");
+
+ VerificationResult verification = verifier.verify(result.getAllLogs());
+
+ System.out.println("\n" + verification.generateReport());
+
+ if (verification.passed()) {
+ System.out.println("\n✓ All verification rules passed!");
+ } else {
+ System.out.println("\n✗ Some verification rules failed:");
+ System.out.println(verification.getFailureMessage());
+ }
+
+ // Additional analysis
+ System.out.println("\n=== Message Exchange Analysis ===");
+
+ // Count message types
+ int sentCount = result.countLogs("Message sent");
+ int receivedCount = result.countLogs("Message received");
+ int fromClientCount = result.countLogs("fromClient=true");
+ int fromServerCount = result.countLogs("fromServer=true");
+
+ System.out.println("Messages sent: " + sentCount);
+ System.out.println("Messages received: " + receivedCount);
+ System.out.println("From client: " + fromClientCount);
+ System.out.println("From server: " + fromServerCount);
+
+ // Verify message flow
+ if (Math.abs(sentCount - receivedCount) <= 1) {
+ System.out.println("✓ Sent/Received messages are balanced");
+ } else {
+ System.out.println("✗ Message imbalance detected");
+ }
+
+ if (Math.abs(fromClientCount - fromServerCount) <= 1) {
+ System.out.println("✓ Client/Server messages are balanced");
+ } else {
+ System.out.println("✗ Client/Server imbalance detected");
+ }
+
+ // Check message IDs
+ System.out.println("\n=== Message ID Sequence ===");
+ logs.stream()
+ .filter(log -> log.getMessage().contains("Message sent"))
+ .limit(10)
+ .forEach(log -> {
+ String msg = log.getMessage();
+ int idStart = msg.indexOf("ID: ") + 4;
+ int idEnd = msg.indexOf(";", idStart);
+ if (idStart > 3 && idEnd > idStart) {
+ String id = msg.substring(idStart, idEnd);
+ System.out.println(" Message ID " + id + " sent at time " +
+ log.getTimestamp());
+ }
+ });
+
+ System.out.println("\n=== Test Complete ===");
+ System.out.println("The Ping-Pong protocol is working correctly!");
+ System.out.println("Messages are being exchanged between client and server.");
+
+ } catch (Exception e) {
+ System.err.println("Test failed with error: " + e.getMessage());
+ e.printStackTrace();
+ } finally {
+ runner.shutdown();
+ }
+ }
+} \ No newline at end of file