summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/core/VSAbstractProcess.java11
-rw-r--r--src/main/java/events/internal/VSProtocolEvent.java59
-rw-r--r--src/main/java/examples/CreateSimpleRaftSimulation.java5
-rw-r--r--src/main/java/protocols/implementations/VSRaftProtocol.java1
-rw-r--r--src/main/java/simulator/builder/SimulationBuilder.java362
-rw-r--r--src/main/java/simulator/builder/SimulationFactory.java115
-rw-r--r--src/main/java/simulator/engine/HeadlessSimulationEngine.java15
-rw-r--r--src/main/java/testing/HeadlessProtocolRunner.java15
-rw-r--r--src/main/java/testing/HeadlessSimulationRunner.java55
-rw-r--r--src/main/java/testing/LogCapture.java29
-rw-r--r--src/main/java/testing/ProtocolVerifier.java65
-rw-r--r--src/main/java/testing/SimulationMetrics.java6
12 files changed, 715 insertions, 23 deletions
diff --git a/src/main/java/core/VSAbstractProcess.java b/src/main/java/core/VSAbstractProcess.java
index 78e7844..ab7444d 100644
--- a/src/main/java/core/VSAbstractProcess.java
+++ b/src/main/java/core/VSAbstractProcess.java
@@ -695,6 +695,17 @@ public abstract class VSAbstractProcess extends VSSerializablePrefs {
for (int i = 0; i < numProtocols; ++i) {
String protocolClassname = (String) objectInputStream.readObject();
+ if (protocolClassname == null || protocolClassname.trim().isEmpty()) {
+ // Handle saved files with null protocol classnames
+ // This can happen if the protocol didn't call setClassname() when it was saved
+ System.err.println("Warning: Found null/empty protocol classname during deserialization, skipping...");
+ // We still need to read the protocol's serialized data to keep the stream in sync
+ // Create a dummy protocol to consume the data
+ VSAbstractProtocol dummyProtocol = new protocols.implementations.VSDummyProtocol();
+ dummyProtocol.init((VSInternalProcess)this);
+ dummyProtocol.deserialize(serialize, objectInputStream);
+ continue;
+ }
VSAbstractProtocol protocol = getProtocolObject_(protocolClassname);
protocol.deserialize(serialize, objectInputStream);
}
diff --git a/src/main/java/events/internal/VSProtocolEvent.java b/src/main/java/events/internal/VSProtocolEvent.java
index 54c8974..5cbe6df 100644
--- a/src/main/java/events/internal/VSProtocolEvent.java
+++ b/src/main/java/events/internal/VSProtocolEvent.java
@@ -5,6 +5,8 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import core.VSInternalProcess;
+import core.VSTask;
+import core.VSTaskManager;
import events.VSAbstractEvent;
import events.VSCopyableEvent;
import events.VSRegisteredEvents;
@@ -106,6 +108,47 @@ public class VSProtocolEvent extends VSAbstractInternalEvent
public void setProtocolClassname(String protocolClassname) {
this.protocolClassname = protocolClassname;
}
+
+ /**
+ * Checks if we should schedule a protocol start task.
+ * We should NOT schedule if the protocol is already scheduled at the current time.
+ * This prevents duplicate execution when loading saved simulations.
+ *
+ * @param process the process to check
+ * @param protocol the protocol to check for
+ * @return true if we should schedule, false if already scheduled
+ */
+ private boolean shouldScheduleProtocolStart(VSInternalProcess process, VSAbstractProtocol protocol) {
+ // Check process-local tasks
+ for (VSTask task : process.getTasks()) {
+ if (task.getEvent() == protocol && task.getTaskTime() == process.getTime()) {
+ // Protocol is already scheduled at this time
+ return false;
+ }
+ }
+
+ // Check global tasks
+ VSTaskManager taskManager = process.getSimulatorCanvas().getTaskManager();
+ try {
+ // Use reflection to access global tasks
+ java.lang.reflect.Field globalTasksField = VSTaskManager.class.getDeclaredField("globalTasks");
+ globalTasksField.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ java.util.Queue<VSTask> globalTasks = (java.util.Queue<VSTask>) globalTasksField.get(taskManager);
+
+ for (VSTask task : globalTasks) {
+ if (task.getEvent() == protocol && task.getTaskTime() == process.getGlobalTime()) {
+ // Protocol is already scheduled at this time
+ return false;
+ }
+ }
+ } catch (Exception e) {
+ // If we can't check, assume we should schedule
+ System.err.println("Warning: Could not check for duplicate protocol tasks: " + e.getMessage());
+ }
+
+ return true;
+ }
/* (non-Javadoc)
* @see events.VSAbstractEvent#onStart()
@@ -137,6 +180,22 @@ public class VSProtocolEvent extends VSAbstractInternalEvent
: prefs.getString("lang.deactivated"));
log(buffer.toString());
+
+ // If this is an activation, schedule the protocol to start immediately
+ // This ensures that protocols with HAS_ON_SERVER_START or HAS_ON_CLIENT_START
+ // will have their onServerStart() or onClientStart() methods called
+ //
+ // However, we should NOT schedule if the protocol is already scheduled to run.
+ // This can happen when loading from a saved simulation where both the activation
+ // event and the resulting protocol task were saved.
+ if (isProtocolActivation && shouldScheduleProtocolStart(internalProcess, protocol)) {
+ // Create a task to start the protocol at the current time
+ VSTask startTask = new VSTask(internalProcess.getTime(),
+ internalProcess,
+ protocol,
+ VSTask.LOCAL);
+ internalProcess.getSimulatorCanvas().getTaskManager().addTask(startTask);
+ }
}
/* (non-Javadoc)
diff --git a/src/main/java/examples/CreateSimpleRaftSimulation.java b/src/main/java/examples/CreateSimpleRaftSimulation.java
index 278824d..ebff53e 100644
--- a/src/main/java/examples/CreateSimpleRaftSimulation.java
+++ b/src/main/java/examples/CreateSimpleRaftSimulation.java
@@ -11,8 +11,9 @@ 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!
+ * The key insight: Raft protocol uses HAS_ON_SERVER_START, so when servers
+ * are activated via VSProtocolEvent, the protocol's onServerStart() method
+ * will be called automatically.
*/
public class CreateSimpleRaftSimulation {
diff --git a/src/main/java/protocols/implementations/VSRaftProtocol.java b/src/main/java/protocols/implementations/VSRaftProtocol.java
index 0d8fa20..72fe540 100644
--- a/src/main/java/protocols/implementations/VSRaftProtocol.java
+++ b/src/main/java/protocols/implementations/VSRaftProtocol.java
@@ -100,6 +100,7 @@ public class VSRaftProtocol extends VSAbstractProtocol {
public VSRaftProtocol() {
super(VSAbstractProtocol.HAS_ON_SERVER_START);
+ setClassname(getClass().toString());
}
@Override
diff --git a/src/main/java/simulator/builder/SimulationBuilder.java b/src/main/java/simulator/builder/SimulationBuilder.java
new file mode 100644
index 0000000..8ac5d04
--- /dev/null
+++ b/src/main/java/simulator/builder/SimulationBuilder.java
@@ -0,0 +1,362 @@
+package simulator.builder;
+
+import simulator.*;
+import core.*;
+import prefs.*;
+import events.*;
+import events.internal.*;
+import serialize.VSSerialize;
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.ArrayList;
+
+/**
+ * Builder for creating DS-Sim simulations programmatically without GUI.
+ *
+ * Example usage:
+ * <pre>
+ * SimulationBuilder builder = new SimulationBuilder()
+ * .withProcesses(5)
+ * .withProtocol("protocols.implementations.VSRaftProtocol")
+ * .activateServers(0, 1, 2)
+ * .activateClients(3, 4)
+ * .addCrashEvent(0, 2000)
+ * .addRecoveryEvent(0, 3000)
+ * .save("saved-simulations/my-raft.dat");
+ * </pre>
+ */
+public class SimulationBuilder {
+
+ private VSDefaultPrefs prefs;
+ private VSSimulator simulator;
+ private VSSimulatorVisualization visualization;
+ private VSTaskManager taskManager;
+ private String protocolClass;
+ private int numProcesses = 3; // default
+ private List<ScheduledTask> scheduledTasks = new ArrayList<>();
+ private int simulationDuration = 10000; // default 10 seconds
+
+ /**
+ * Internal class to hold task scheduling information
+ */
+ private static class ScheduledTask {
+ long time;
+ int processId;
+ VSAbstractEvent event;
+ boolean isGlobalTimed;
+
+ ScheduledTask(long time, int processId, VSAbstractEvent event, boolean isGlobalTimed) {
+ this.time = time;
+ this.processId = processId;
+ this.event = event;
+ this.isGlobalTimed = isGlobalTimed;
+ }
+ }
+
+ /**
+ * Initialize the builder with default preferences
+ */
+ public SimulationBuilder() throws Exception {
+ // Initialize preferences
+ prefs = new VSDefaultPrefs();
+ prefs.fillWithDefaults();
+
+ // Initialize registered events
+ VSRegisteredEvents.init(prefs);
+ }
+
+ /**
+ * Set the simulation duration in milliseconds
+ */
+ public SimulationBuilder withDuration(int durationMs) {
+ this.simulationDuration = durationMs;
+ return this;
+ }
+
+ /**
+ * Set the number of processes in the simulation
+ */
+ public SimulationBuilder withProcesses(int count) {
+ this.numProcesses = count;
+ return this;
+ }
+
+ /**
+ * Set the protocol class to use
+ */
+ public SimulationBuilder withProtocol(String protocolClassName) {
+ this.protocolClass = protocolClassName;
+ return this;
+ }
+
+ /**
+ * Activate protocol as server on specified processes
+ */
+ public SimulationBuilder activateServers(int... processIds) {
+ for (int pid : processIds) {
+ VSProtocolEvent event = new VSProtocolEvent();
+ event.onInit(); // Initialize the event first
+ setProtocolClassname(event, protocolClass);
+ setIsServer(event, true);
+
+ scheduledTasks.add(new ScheduledTask(0, pid, event, true));
+ }
+ return this;
+ }
+
+ /**
+ * Activate protocol as client on specified processes
+ */
+ public SimulationBuilder activateClients(int... processIds) {
+ return activateClients(500, processIds); // default delay
+ }
+
+ /**
+ * Activate protocol as client on specified processes with custom start time
+ */
+ public SimulationBuilder activateClients(long startTime, int... processIds) {
+ for (int i = 0; i < processIds.length; i++) {
+ VSProtocolEvent event = new VSProtocolEvent();
+ event.onInit(); // Initialize the event first
+ setProtocolClassname(event, protocolClass);
+ setIsServer(event, false);
+
+ // Stagger client starts
+ long time = startTime + (i * 200);
+ scheduledTasks.add(new ScheduledTask(time, processIds[i], event, true));
+ }
+ return this;
+ }
+
+ /**
+ * Add a process crash event
+ */
+ public SimulationBuilder addCrashEvent(int processId, long time) {
+ try {
+ // Use reflection to create crash event
+ Class<?> crashClass = Class.forName("events.implementations.VSProcessCrashEvent");
+ VSAbstractEvent crashEvent = (VSAbstractEvent) crashClass.getDeclaredConstructor().newInstance();
+ scheduledTasks.add(new ScheduledTask(time, processId, crashEvent, true));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create crash event", e);
+ }
+ return this;
+ }
+
+ /**
+ * Add a process recovery event
+ */
+ public SimulationBuilder addRecoveryEvent(int processId, long time) {
+ try {
+ // Use reflection to create recovery event
+ Class<?> recoverClass = Class.forName("events.implementations.VSProcessRecoverEvent");
+ VSAbstractEvent recoverEvent = (VSAbstractEvent) recoverClass.getDeclaredConstructor().newInstance();
+ scheduledTasks.add(new ScheduledTask(time, processId, recoverEvent, true));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create recovery event", e);
+ }
+ return this;
+ }
+
+ /**
+ * Add a custom event
+ */
+ public SimulationBuilder addEvent(String eventClassName, int processId, long time) {
+ try {
+ Class<?> eventClass = Class.forName(eventClassName);
+ VSAbstractEvent event = (VSAbstractEvent) eventClass.getDeclaredConstructor().newInstance();
+ scheduledTasks.add(new ScheduledTask(time, processId, event, true));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create event: " + eventClassName, e);
+ }
+ return this;
+ }
+
+ /**
+ * Set protocol classname using reflection (since field is private)
+ */
+ private void setProtocolClassname(VSProtocolEvent event, String classname) {
+ try {
+ Field field = VSProtocolEvent.class.getDeclaredField("protocolClassname");
+ field.setAccessible(true);
+ field.set(event, classname);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to set protocol classname", e);
+ }
+ }
+
+ /**
+ * Set isServer flag using reflection (field is called isClientProtocol)
+ */
+ private void setIsServer(VSProtocolEvent event, boolean isServer) {
+ try {
+ // The field is actually called isClientProtocol, and server = !client
+ Field field = VSProtocolEvent.class.getDeclaredField("isClientProtocol");
+ field.setAccessible(true);
+ field.set(event, !isServer); // Invert: server means NOT client
+
+ // Also set protocol activation to true
+ Field activationField = VSProtocolEvent.class.getDeclaredField("isProtocolActivation");
+ activationField.setAccessible(true);
+ activationField.set(event, true);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to set protocol flags", e);
+ }
+ }
+
+ /**
+ * Build the simulation (must be called before save)
+ */
+ private void build() throws Exception {
+ if (simulator != null) {
+ return; // Already built
+ }
+
+ // Set simulation duration
+ prefs.setInteger("sim.seconds", simulationDuration / 1000);
+
+ // Set network delay parameters for message delivery
+ prefs.setInteger("process.msg.delay.min", 10); // 10ms minimum delay
+ prefs.setInteger("process.msg.delay.max", 50); // 50ms maximum delay
+
+ // Create simulator without frame for headless
+ simulator = new VSSimulator(prefs, null);
+
+ // Create visualization without GUI
+ VSLogging logging = new VSLogging();
+ visualization = new VSSimulatorVisualization(prefs, simulator, logging);
+
+ // Set visualization in simulator using reflection
+ Field vizField = VSSimulator.class.getDeclaredField("simulatorVisualization");
+ vizField.setAccessible(true);
+ vizField.set(simulator, visualization);
+
+ // Add processes if needed (default is 3)
+ Method addProcessMethod = VSSimulatorVisualization.class.getDeclaredMethod("addProcess");
+ addProcessMethod.setAccessible(true);
+
+ // Remove default processes if we want fewer
+ if (numProcesses < 3) {
+ Field processesField = VSSimulatorVisualization.class.getDeclaredField("processes");
+ processesField.setAccessible(true);
+ ArrayList<VSInternalProcess> processes = (ArrayList<VSInternalProcess>) processesField.get(visualization);
+ while (processes.size() > numProcesses) {
+ processes.remove(processes.size() - 1);
+ }
+ }
+
+ // Add more processes if needed
+ for (int i = 3; i < numProcesses; i++) {
+ addProcessMethod.invoke(visualization);
+ }
+
+ // Get task manager
+ taskManager = visualization.getTaskManager();
+
+ // Initialize all events with their processes
+ for (ScheduledTask st : scheduledTasks) {
+ VSInternalProcess process = visualization.getProcess(st.processId);
+ st.event.init(process);
+
+ // For protocol events, update the shortname after init
+ if (st.event instanceof VSProtocolEvent) {
+ VSProtocolEvent protocolEvent = (VSProtocolEvent) st.event;
+ // Force shortname update by calling the method via reflection
+ try {
+ Method createShortname = VSProtocolEvent.class.getDeclaredMethod("createShortname", String.class);
+ createShortname.setAccessible(true);
+ String shortname = (String) createShortname.invoke(protocolEvent, (String)null);
+ protocolEvent.setShortname(shortname);
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+
+ // Create task
+ VSTask task = new VSTask(st.time, process, st.event,
+ st.isGlobalTimed ? VSTask.GLOBAL : VSTask.LOCAL);
+ taskManager.addTask(task);
+ }
+ }
+
+ /**
+ * Save the simulation to a file
+ */
+ public SimulationBuilder save(String filename) throws Exception {
+ build();
+
+ File outputFile = new File(filename);
+ outputFile.getParentFile().mkdirs();
+
+ VSSerialize serialize = new VSSerialize();
+
+ // Save using the serializer
+ try {
+ FileOutputStream fos = new FileOutputStream(outputFile);
+ ObjectOutputStream oos = new ObjectOutputStream(fos);
+
+ // Create serializable prefs from our prefs
+ VSSerializablePrefs serializablePrefs = new VSSerializablePrefs();
+
+ // Copy all preferences
+ for (String key : prefs.getIntegerKeySet()) {
+ serializablePrefs.initInteger(key, prefs.getInteger(key));
+ }
+ for (String key : prefs.getBooleanKeySet()) {
+ serializablePrefs.initBoolean(key, prefs.getBoolean(key));
+ }
+ for (String key : prefs.getStringKeySet()) {
+ serializablePrefs.initString(key, prefs.getString(key));
+ }
+ for (String key : prefs.getFloatKeySet()) {
+ serializablePrefs.initFloat(key, prefs.getFloat(key));
+ }
+ for (String key : prefs.getColorKeySet()) {
+ serializablePrefs.initColor(key, prefs.getColor(key));
+ }
+ for (String key : prefs.getVectorKeySet()) {
+ serializablePrefs.initVector(key, prefs.getVector(key));
+ }
+ for (String key : prefs.getLongKeySet()) {
+ serializablePrefs.initLong(key, prefs.getLong(key));
+ }
+
+ // Serialize preferences first
+ serializablePrefs.serialize(serialize, oos);
+
+ // Then serialize simulator
+ simulator.serialize(serialize, oos);
+
+ oos.close();
+ fos.close();
+
+ System.out.println("Simulation saved to: " + outputFile.getAbsolutePath());
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to save simulation", e);
+ }
+
+ return this;
+ }
+
+ /**
+ * Get the built simulator (for testing/verification)
+ */
+ public VSSimulator getSimulator() throws Exception {
+ build();
+ return simulator;
+ }
+
+ /**
+ * Fluent API for common protocol setups
+ */
+ public static class Protocols {
+ public static final String RAFT = "protocols.implementations.VSRaftProtocol";
+ public static final String PING_PONG = "protocols.implementations.VSPingPongProtocol";
+ public static final String BERKLEY_TIME = "protocols.implementations.VSBerkelyTimeProtocol";
+ public static final String BROADCAST = "protocols.implementations.VSBroadcastProtocol";
+ public static final String ONE_PHASE_COMMIT = "protocols.implementations.VSOnePhaseCommitProtocol";
+ public static final String TWO_PHASE_COMMIT = "protocols.implementations.VSTwoPhaseCommitProtocol";
+ public static final String RELIABLE_MULTICAST = "protocols.implementations.VSReliableMulticastProtocol";
+ }
+} \ No newline at end of file
diff --git a/src/main/java/simulator/builder/SimulationFactory.java b/src/main/java/simulator/builder/SimulationFactory.java
new file mode 100644
index 0000000..c06be00
--- /dev/null
+++ b/src/main/java/simulator/builder/SimulationFactory.java
@@ -0,0 +1,115 @@
+package simulator.builder;
+
+import java.util.stream.IntStream;
+
+/**
+ * Factory for creating common simulation patterns using SimulationBuilder.
+ * Provides convenience methods for standard distributed systems scenarios.
+ */
+public class SimulationFactory {
+
+ /**
+ * Create a standard Raft consensus simulation
+ * @param numServers Number of Raft servers (minimum 3 for consensus)
+ * @param numClients Number of client processes
+ * @return Configured SimulationBuilder
+ */
+ public static SimulationBuilder createRaftSimulation(int numServers, int numClients) throws Exception {
+ if (numServers < 3) {
+ throw new IllegalArgumentException("Raft requires at least 3 servers for consensus");
+ }
+
+ return new SimulationBuilder()
+ .withProcesses(numServers + numClients)
+ .withProtocol(SimulationBuilder.Protocols.RAFT)
+ .withDuration(15000) // 15 seconds to see leader election
+ .activateServers(IntStream.range(0, numServers).toArray())
+ .activateClients(500, IntStream.range(numServers, numServers + numClients).toArray());
+ }
+
+ /**
+ * Create a Raft simulation with fault tolerance testing
+ * @param numServers Number of Raft servers
+ * @return Configured SimulationBuilder with crash/recovery events
+ */
+ public static SimulationBuilder createRaftFaultToleranceSimulation(int numServers) throws Exception {
+ return createRaftSimulation(numServers, 0)
+ .withDuration(30000) // 30 seconds for fault testing
+ .addCrashEvent(0, 5000) // Crash leader after 5s
+ .addRecoveryEvent(0, 10000) // Recover after 10s
+ .addCrashEvent(1, 15000) // Crash another server
+ .addRecoveryEvent(1, 20000); // Recover after 20s
+ }
+
+ /**
+ * Create a simple ping-pong simulation
+ * @param numProcesses Number of processes to ping-pong between
+ * @return Configured SimulationBuilder
+ */
+ public static SimulationBuilder createPingPongSimulation(int numProcesses) throws Exception {
+ return new SimulationBuilder()
+ .withProcesses(numProcesses)
+ .withProtocol(SimulationBuilder.Protocols.PING_PONG)
+ .withDuration(5000)
+ .activateServers(IntStream.range(0, numProcesses).toArray());
+ }
+
+ /**
+ * Create a Berkeley time synchronization simulation
+ * @param numProcesses Number of processes to synchronize
+ * @return Configured SimulationBuilder
+ */
+ public static SimulationBuilder createBerkeleyTimeSimulation(int numProcesses) throws Exception {
+ if (numProcesses < 2) {
+ throw new IllegalArgumentException("Berkeley algorithm needs at least 2 processes");
+ }
+
+ return new SimulationBuilder()
+ .withProcesses(numProcesses)
+ .withProtocol(SimulationBuilder.Protocols.BERKLEY_TIME)
+ .withDuration(10000)
+ .activateServers(0) // First process is time server
+ .activateClients(IntStream.range(1, numProcesses).toArray());
+ }
+
+ /**
+ * Create a two-phase commit simulation
+ * @param numParticipants Number of participant processes
+ * @return Configured SimulationBuilder
+ */
+ public static SimulationBuilder createTwoPhaseCommitSimulation(int numParticipants) throws Exception {
+ return new SimulationBuilder()
+ .withProcesses(numParticipants + 1) // +1 for coordinator
+ .withProtocol(SimulationBuilder.Protocols.TWO_PHASE_COMMIT)
+ .withDuration(10000)
+ .activateServers(0) // Process 0 is coordinator
+ .activateClients(300, IntStream.range(1, numParticipants + 1).toArray());
+ }
+
+ /**
+ * Create a reliable multicast simulation
+ * @param numProcesses Number of processes in the multicast group
+ * @return Configured SimulationBuilder
+ */
+ public static SimulationBuilder createReliableMulticastSimulation(int numProcesses) throws Exception {
+ return new SimulationBuilder()
+ .withProcesses(numProcesses)
+ .withProtocol(SimulationBuilder.Protocols.RELIABLE_MULTICAST)
+ .withDuration(10000)
+ .activateServers(IntStream.range(0, numProcesses).toArray());
+ }
+
+ /**
+ * Create a broadcast protocol simulation
+ * @param numProcesses Number of processes
+ * @return Configured SimulationBuilder
+ */
+ public static SimulationBuilder createBroadcastSimulation(int numProcesses) throws Exception {
+ return new SimulationBuilder()
+ .withProcesses(numProcesses)
+ .withProtocol(SimulationBuilder.Protocols.BROADCAST)
+ .withDuration(8000)
+ .activateServers(0) // First process broadcasts
+ .activateClients(IntStream.range(1, numProcesses).toArray());
+ }
+} \ No newline at end of file
diff --git a/src/main/java/simulator/engine/HeadlessSimulationEngine.java b/src/main/java/simulator/engine/HeadlessSimulationEngine.java
index fa6dde8..921cbb1 100644
--- a/src/main/java/simulator/engine/HeadlessSimulationEngine.java
+++ b/src/main/java/simulator/engine/HeadlessSimulationEngine.java
@@ -38,12 +38,7 @@ public class HeadlessSimulationEngine extends AbstractSimulationEngine {
VSInternalProcess sendingProcess = (VSInternalProcess) message.getSendingProcess();
boolean recvOwn = prefs.getBoolean("sim.message.own.recv");
- // Debug logging
- if (loging != null) {
- loging.log("Message " + message.getMessageID() + " scheduled for delivery at time " +
- deliveryTime + " (sent at globalTime=" + sendingProcess.getGlobalTime() +
- ", duration=" + (deliveryTime - sendingProcess.getGlobalTime()) + "ms)");
- }
+ // Debug logging removed to avoid affecting test behavior
// Schedule delivery to all processes
for (VSInternalProcess receiverProcess : processes) {
@@ -57,7 +52,13 @@ public class HeadlessSimulationEngine extends AbstractSimulationEngine {
// Create receive event for this process
VSMessageReceiveEvent receiveEvent = new VSMessageReceiveEvent(message);
VSTask task = new VSTask(deliveryTime, receiverProcess, receiveEvent, VSTask.GLOBAL);
- taskManager.addTask(task);
+
+ // Important: Use the task manager from the receiving process's simulator canvas
+ // This ensures tasks are added to the correct task manager that's being run
+ VSTaskManager actualTaskManager = receiverProcess.getSimulatorCanvas().getTaskManager();
+ actualTaskManager.addTask(task);
+
+ // Debug logging removed
}
}
diff --git a/src/main/java/testing/HeadlessProtocolRunner.java b/src/main/java/testing/HeadlessProtocolRunner.java
index daf96aa..a6098de 100644
--- a/src/main/java/testing/HeadlessProtocolRunner.java
+++ b/src/main/java/testing/HeadlessProtocolRunner.java
@@ -50,6 +50,21 @@ public class HeadlessProtocolRunner {
System.out.println(" Log entries: " + result.getMetrics().getTotalLogCount());
System.out.println(" Messages per process: " + result.getMetrics().getProcessMessageCounts());
+ // Count total messages sent
+ int totalMessages = result.getMetrics().getTotalMessageCount();
+ System.out.println(" Total messages sent: " + totalMessages);
+
+ // Check if any messages were sent
+ if (totalMessages == 0) {
+ System.err.println("\n⚠️ WARNING: No messages were sent during simulation!");
+ System.err.println(" This indicates the protocol may not be functioning correctly.");
+ if (!verbose) {
+ System.err.println(" Re-run with -Dds.sim.verbose=true for detailed output.");
+ }
+ // Mark as failure
+ throw new RuntimeException("Protocol test failed: No messages sent");
+ }
+
System.out.println();
} catch (Exception e) {
System.err.println("✗ FAILED: " + e.getMessage());
diff --git a/src/main/java/testing/HeadlessSimulationRunner.java b/src/main/java/testing/HeadlessSimulationRunner.java
index 9d2274c..6279fa9 100644
--- a/src/main/java/testing/HeadlessSimulationRunner.java
+++ b/src/main/java/testing/HeadlessSimulationRunner.java
@@ -26,6 +26,11 @@ public class HeadlessSimulationRunner {
public HeadlessSimulationRunner() {
this.prefs = new VSDefaultPrefs();
this.prefs.fillWithDefaults();
+
+ // Set reasonable message delays for testing (10-50ms instead of 500-2000ms)
+ this.prefs.initLong("message.sendingtime.min", 10);
+ this.prefs.initLong("message.sendingtime.max", 50);
+
VSRegisteredEvents.init(prefs);
this.executor = Executors.newSingleThreadExecutor();
}
@@ -55,14 +60,20 @@ public class HeadlessSimulationRunner {
simulator = loaded.getSimulator();
viz = loaded.getVisualization();
+ // Update message delays on all processes after loading
+ for (int i = 0; i < viz.getNumProcesses(); i++) {
+ VSInternalProcess process = viz.getProcess(i);
+ if (process != null) {
+ process.initLong("message.sendingtime.min", 10);
+ process.initLong("message.sendingtime.max", 50);
+ }
+ }
+
if (simulator == null || viz == null) {
throw new IllegalStateException("Failed to load simulation");
}
- // Set up headless message handlers for all processes
- setupHeadlessMessageHandlers(viz);
-
- // Install log capture
+ // Install log capture first
logCapture = new LogCapture();
logCapture.setPrintLogs(printLogs);
if (listener != null) {
@@ -70,6 +81,9 @@ public class HeadlessSimulationRunner {
}
installLogCapture();
+ // Set up headless message handlers for all processes (after log capture is ready)
+ setupHeadlessMessageHandlers(viz);
+
// Get the simulation's configured end time
long untilTime = viz.getUntilTime();
long actualMaxTime = Math.min(maxTime, untilTime);
@@ -187,10 +201,11 @@ public class HeadlessSimulationRunner {
lastActiveTime = currentTime;
} else {
noActivityCount++;
- // If no activity for 3000ms (3 seconds) of simulation time, stop
- // This accounts for message delivery times of 500-2000ms plus some buffer
- if (noActivityCount > 3000 && (currentTime - lastActiveTime) > 3000) {
- System.out.println("No activity detected for 3 seconds - simulation complete at time " + simulatorTime);
+ // If no activity for 5000ms (5 seconds) of simulation time, stop
+ // This accounts for message delivery times of 500-2000ms plus extra buffer
+ // to ensure all messages are delivered
+ if (noActivityCount > 5000 && (currentTime - lastActiveTime) > 5000) {
+ System.out.println("No activity detected for 5 seconds - simulation complete at time " + simulatorTime);
break;
}
}
@@ -208,10 +223,19 @@ public class HeadlessSimulationRunner {
private boolean hasPendingActivity(VSTaskManager taskManager, Field globalTasksField, long currentTime) {
try {
- // Check global tasks
+ // Check global tasks - but also check if any are scheduled for future times
Queue<?> globalTasks = (Queue<?>) globalTasksField.get(taskManager);
if (globalTasks != null && !globalTasks.isEmpty()) {
- return true; // If any global tasks exist, keep running
+ // Check if any tasks are scheduled for the future
+ for (Object obj : globalTasks) {
+ VSTask task = (VSTask) obj;
+ if (task.getTaskTime() > currentTime) {
+ // There's a future task scheduled, keep running
+ return true;
+ }
+ }
+ // If all tasks are in the past or present, they should execute now
+ return true;
}
// Check process-specific tasks
@@ -220,12 +244,19 @@ public class HeadlessSimulationRunner {
if (process != null) {
Queue<VSTask> tasks = process.getTasks();
if (tasks != null && !tasks.isEmpty()) {
- return true; // If any process tasks exist, keep running
+ // Check if any tasks are scheduled for the future
+ for (VSTask task : tasks) {
+ if (task.getTaskTime() > process.getTime()) {
+ return true;
+ }
+ }
+ // If all tasks are ready to run, keep going
+ return true;
}
}
}
- // Check for messages in transit
+ // Check for messages in transit (visualization lines)
Field messageLinesField = VSSimulatorVisualization.class.getDeclaredField("messageLines");
messageLinesField.setAccessible(true);
LinkedList<?> messageLines = (LinkedList<?>) messageLinesField.get(viz);
diff --git a/src/main/java/testing/LogCapture.java b/src/main/java/testing/LogCapture.java
index 97bb127..ddd0ad0 100644
--- a/src/main/java/testing/LogCapture.java
+++ b/src/main/java/testing/LogCapture.java
@@ -30,6 +30,17 @@ public class LogCapture extends VSLogging {
this.printLogs = printLogs;
}
+ public void setSimulatorCanvas(VSSimulatorVisualization viz) {
+ // Store reference for process count
+ try {
+ Field field = VSLogging.class.getDeclaredField("simulatorVisualization");
+ field.setAccessible(true);
+ field.set(this, viz);
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+
public void setLogPrefix(String prefix) {
this.logPrefix = prefix;
}
@@ -136,9 +147,23 @@ public class LogCapture extends VSLogging {
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());
+
+ // Initialize counts for all processes
+ VSSimulatorVisualization viz = getSimulatorVisualization();
+ if (viz != null) {
+ for (int i = 0; i < viz.getNumProcesses(); i++) {
+ counts.put(i, 0);
+ }
}
+
+ // Count messages from all captured logs
+ for (LogEntry log : capturedLogs) {
+ if (log.getMessage().contains("Message sent")) {
+ int processNum = log.getProcessNum();
+ counts.put(processNum, counts.getOrDefault(processNum, 0) + 1);
+ }
+ }
+
return counts;
}
diff --git a/src/main/java/testing/ProtocolVerifier.java b/src/main/java/testing/ProtocolVerifier.java
index 19ed1f2..e5338d4 100644
--- a/src/main/java/testing/ProtocolVerifier.java
+++ b/src/main/java/testing/ProtocolVerifier.java
@@ -80,6 +80,29 @@ public class ProtocolVerifier {
}
/**
+ * Expect at least n messages to be sent during the simulation.
+ */
+ public ProtocolVerifier expectAtLeastNMessages(int minMessages) {
+ rules.add(new MessageCountRule(minMessages, Integer.MAX_VALUE));
+ return this;
+ }
+
+ /**
+ * Expect exactly n messages to be sent during the simulation.
+ */
+ public ProtocolVerifier expectExactlyNMessages(int count) {
+ rules.add(new MessageCountRule(count, count));
+ return this;
+ }
+
+ /**
+ * Expect messages to be sent (at least 1).
+ */
+ public ProtocolVerifier expectMessages() {
+ return expectAtLeastNMessages(1);
+ }
+
+ /**
* Verify all rules against the provided logs.
*/
public VerificationResult verify(List<LogEntry> logs) {
@@ -240,4 +263,46 @@ public class ProtocolVerifier {
return new RuleResult(passed, message, matches);
}
}
+
+ /**
+ * Rule that verifies message count.
+ */
+ private static class MessageCountRule implements VerificationRule {
+ private final int minCount;
+ private final int maxCount;
+ private final String description;
+
+ public MessageCountRule(int minCount, int maxCount) {
+ this.minCount = minCount;
+ this.maxCount = maxCount;
+ this.description = String.format(
+ "Message count should be %s",
+ minCount == maxCount ?
+ String.valueOf(minCount) :
+ minCount + "-" + (maxCount == Integer.MAX_VALUE ? "∞" : maxCount)
+ );
+ }
+
+ @Override
+ public RuleResult verify(List<LogEntry> logs) {
+ int messageCount = 0;
+ List<LogEntry> messageLogs = new ArrayList<>();
+
+ // Count all "Message sent" logs
+ for (LogEntry log : logs) {
+ if (log.getMessage().contains("Message sent")) {
+ messageCount++;
+ messageLogs.add(log);
+ }
+ }
+
+ boolean passed = messageCount >= minCount && messageCount <= maxCount;
+ String message = String.format(
+ "%s (found %d messages)",
+ description, messageCount
+ );
+
+ return new RuleResult(passed, message, messageLogs);
+ }
+ }
} \ No newline at end of file
diff --git a/src/main/java/testing/SimulationMetrics.java b/src/main/java/testing/SimulationMetrics.java
index 2b80631..dc8fc39 100644
--- a/src/main/java/testing/SimulationMetrics.java
+++ b/src/main/java/testing/SimulationMetrics.java
@@ -44,4 +44,10 @@ public class SimulationMetrics {
return (double) totalProcessMessages / numProcesses;
}
+
+ public int getTotalMessageCount() {
+ return processMessageCounts.values().stream()
+ .mapToInt(Integer::intValue)
+ .sum();
+ }
} \ No newline at end of file