diff options
| -rw-r--r-- | saved-simulations/raft.dat | bin | 14533 -> 15305 bytes | |||
| -rw-r--r-- | src/main/java/simulator/builder/SimulationFactory.java | 4 | ||||
| -rw-r--r-- | src/test/java/core/VSTaskManagerCrashRecoveryIntegrationTest.java | 153 | ||||
| -rw-r--r-- | src/test/java/simulator/builder/SimulationBuilderTest.java | 15 |
4 files changed, 171 insertions, 1 deletions
diff --git a/saved-simulations/raft.dat b/saved-simulations/raft.dat Binary files differindex d64aa13..c54c0c5 100644 --- a/saved-simulations/raft.dat +++ b/saved-simulations/raft.dat diff --git a/src/main/java/simulator/builder/SimulationFactory.java b/src/main/java/simulator/builder/SimulationFactory.java index 570a0d0..0695a11 100644 --- a/src/main/java/simulator/builder/SimulationFactory.java +++ b/src/main/java/simulator/builder/SimulationFactory.java @@ -103,6 +103,8 @@ public class SimulationFactory { .setProtocolLong(1, "electionJitter", 0) .setProtocolLong(2, "electionTimeout", 12000) .setProtocolLong(2, "electionJitter", 0) - .addCrashEvent(0, 3500); + .addCrashEvent(0, 3500) + .addRecoveryEvent(0, 12000) + .addCrashEvent(2, 20000); } } diff --git a/src/test/java/core/VSTaskManagerCrashRecoveryIntegrationTest.java b/src/test/java/core/VSTaskManagerCrashRecoveryIntegrationTest.java new file mode 100644 index 0000000..81ceeb8 --- /dev/null +++ b/src/test/java/core/VSTaskManagerCrashRecoveryIntegrationTest.java @@ -0,0 +1,153 @@ +package core; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import events.implementations.VSProcessCrashEvent; +import events.implementations.VSProcessRecoverEvent; +import simulator.VSSimulator; +import simulator.VSSimulatorVisualization; +import simulator.builder.SimulationBuilder; +import testing.HeadlessLoader; + +class VSTaskManagerCrashRecoveryIntegrationTest { + private static final long ADVANCE_STEP_MS = 1L; + + private VSSimulator simulatorToStop; + private VSSimulator loadedSimulatorToStop; + + @AfterEach + void tearDown() { + stopSimulatorThread(simulatorToStop); + stopSimulatorThread(loadedSimulatorToStop); + } + + @Test + @DisplayName("Runtime supports recover then later crash of a different process") + void runtimeSupportsRecoverThenCrashAnotherProcess() throws Exception { + VSSimulator simulator = new SimulationBuilder() + .withProcesses(3) + .withDuration(1000) + .getSimulator(); + simulatorToStop = simulator; + + VSSimulatorVisualization visualization = simulator.getSimulatorCanvas(); + VSInternalProcess process0 = visualization.getProcess(0); + VSInternalProcess process1 = visualization.getProcess(1); + + VSTaskManager taskManager = visualization.getTaskManager(); + taskManager.addTask(new VSTask(5, process0, new VSProcessCrashEvent(), VSTask.GLOBAL)); + taskManager.addTask(new VSTask(10, process0, new VSProcessRecoverEvent(), VSTask.GLOBAL)); + taskManager.addTask(new VSTask(15, process1, new VSProcessCrashEvent(), VSTask.GLOBAL)); + + runUntil(visualization, 20); + + assertFalse(process0.isCrashed(), "process 0 should have recovered"); + assertTrue(process1.isCrashed(), "process 1 should crash after process 0 recovers"); + } + + @Test + @DisplayName("Saved replay preserves recover then later crash of another process") + void savedReplayPreservesRecoverThenCrashAnotherProcess() throws Exception { + Path datFile = Files.createTempFile("crash-recover-replay", ".dat"); + + VSSimulator builtSimulator = new SimulationBuilder() + .withProcesses(3) + .withDuration(1000) + .addCrashEvent(0, 5) + .addRecoveryEvent(0, 10) + .addCrashEvent(1, 15) + .save(datFile.toString()) + .getSimulator(); + simulatorToStop = builtSimulator; + + HeadlessLoader.LoadedSimulation loaded = + HeadlessLoader.load(datFile.toString(), builtSimulator.getPrefs()); + loadedSimulatorToStop = loaded.getSimulator(); + + VSSimulatorVisualization visualization = loaded.getVisualization(); + runUntil(visualization, 20); + + assertFalse(visualization.getProcess(0).isCrashed(), + "process 0 should recover after replay load"); + assertTrue(visualization.getProcess(1).isCrashed(), + "process 1 should still crash later in the replay"); + } + + @Test + @DisplayName("Live GUI-style event injection supports recover and later crash") + void liveEventInjectionSupportsRecoverAndLaterCrash() throws Exception { + VSSimulator simulator = new SimulationBuilder() + .withProcesses(3) + .withDuration(1000) + .getSimulator(); + simulatorToStop = simulator; + + VSSimulatorVisualization visualization = simulator.getSimulatorCanvas(); + VSInternalProcess process0 = visualization.getProcess(0); + VSInternalProcess process1 = visualization.getProcess(1); + VSTaskManager taskManager = visualization.getTaskManager(); + + runUntil(visualization, 5); + taskManager.addTask(new VSTask(process0.getGlobalTime(), process0, + new VSProcessCrashEvent(), VSTask.GLOBAL)); + runUntil(visualization, 6); + assertTrue(process0.isCrashed(), "process 0 should crash from live event"); + + taskManager.addTask(new VSTask(process0.getGlobalTime(), process0, + new VSProcessRecoverEvent(), VSTask.GLOBAL)); + runUntil(visualization, 7); + assertFalse(process0.isCrashed(), "process 0 should recover from live event"); + + taskManager.addTask(new VSTask(process1.getGlobalTime(), process1, + new VSProcessCrashEvent(), VSTask.GLOBAL)); + runUntil(visualization, 8); + assertTrue(process1.isCrashed(), "process 1 should crash after process 0 recovers"); + } + + private void runUntil(VSSimulatorVisualization visualization, long targetTime) + throws Exception { + setBooleanField(visualization, "isPaused", false); + setBooleanField(visualization, "hasFinished", false); + setDoubleField(visualization, "clockSpeed", 1.0d); + Method updateSimulator = VSSimulatorVisualization.class.getDeclaredMethod( + "updateSimulator", long.class, long.class); + updateSimulator.setAccessible(true); + + long wallTime = visualization.getTime(); + while (visualization.getTime() < targetTime) { + long nextWallTime = wallTime + ADVANCE_STEP_MS; + updateSimulator.invoke(visualization, nextWallTime, wallTime); + wallTime = nextWallTime; + } + } + + private void setBooleanField(Object target, String fieldName, boolean value) + throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.setBoolean(target, value); + } + + private void setDoubleField(Object target, String fieldName, double value) + throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.setDouble(target, value); + } + + private void stopSimulatorThread(VSSimulator simulator) { + if (simulator != null) { + simulator.getSimulatorCanvas().stopThread(); + } + } +} diff --git a/src/test/java/simulator/builder/SimulationBuilderTest.java b/src/test/java/simulator/builder/SimulationBuilderTest.java index a5b25a0..c2d6511 100644 --- a/src/test/java/simulator/builder/SimulationBuilderTest.java +++ b/src/test/java/simulator/builder/SimulationBuilderTest.java @@ -89,6 +89,9 @@ class SimulationBuilderTest { StandardCharsets.ISO_8859_1); assertTrue(content.contains("VSRaftProtocol"), "Should contain Raft protocol"); assertTrue(content.contains("VSProcessCrashEvent"), "Should contain crash event"); + assertTrue(content.contains("VSProcessRecoverEvent"), "Should contain recovery event"); + assertTrue(countOccurrences(content, "VSProcessCrashEvent") >= 2, + "Should contain two crash events for different processes"); } @Test @@ -125,4 +128,16 @@ class SimulationBuilderTest { SimulationFactory.createBerkeleyTimeSimulation(1); // Too few processes }); } + + private int countOccurrences(String content, String needle) { + int count = 0; + int index = 0; + + while ((index = content.indexOf(needle, index)) != -1) { + count++; + index += needle.length(); + } + + return count; + } } |
