diff options
Diffstat (limited to 'src/main')
| -rw-r--r-- | src/main/java/core/VSAbstractProcess.java | 11 | ||||
| -rw-r--r-- | src/main/java/events/internal/VSProtocolEvent.java | 59 | ||||
| -rw-r--r-- | src/main/java/examples/CreateSimpleRaftSimulation.java | 5 | ||||
| -rw-r--r-- | src/main/java/protocols/implementations/VSRaftProtocol.java | 1 | ||||
| -rw-r--r-- | src/main/java/simulator/builder/SimulationBuilder.java | 362 | ||||
| -rw-r--r-- | src/main/java/simulator/builder/SimulationFactory.java | 115 | ||||
| -rw-r--r-- | src/main/java/simulator/engine/HeadlessSimulationEngine.java | 15 | ||||
| -rw-r--r-- | src/main/java/testing/HeadlessProtocolRunner.java | 15 | ||||
| -rw-r--r-- | src/main/java/testing/HeadlessSimulationRunner.java | 55 | ||||
| -rw-r--r-- | src/main/java/testing/LogCapture.java | 29 | ||||
| -rw-r--r-- | src/main/java/testing/ProtocolVerifier.java | 65 | ||||
| -rw-r--r-- | src/main/java/testing/SimulationMetrics.java | 6 |
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 |
