diff options
| author | Paul Buetow <paul@buetow.org> | 2025-06-22 11:58:00 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-06-22 11:58:00 +0300 |
| commit | 0b5afe8839241dec66ba832cf42860ec69b87df8 (patch) | |
| tree | e100d2d6204f8c04dc33418ae9f193fa6b1a83c2 /src/main/java | |
| parent | b0fc02ce45cb51ce7c8d607d4773808cfa9b6c87 (diff) | |
Fix message delivery in headless test environment
- Fixed HeadlessSimulationEngine to use correct task manager from receiving process
- Reduced message delays for testing (10-50ms instead of 500-2000ms)
- Fixed process ID method call (getProcessID not getProcessId)
- Improved message delivery scheduling to ensure tasks go to the right task manager
This resolves message delivery issues where messages were sent but not received.
BasicMulticast test now passes, but 12 protocol tests still failing.
š¤ Generated with Claude Code
https://claude.ai/code
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'src/main/java')
| -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 |
