diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-27 15:16:59 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-27 15:16:59 +0200 |
| commit | ca9d7633d76871cb0ea00dd46af351daf8ef4895 (patch) | |
| tree | c45ff231af84400587a365cfe1d1663b98078b35 /src/test | |
| parent | 7088d292893e3e20324b04527e93de9b1c73f6aa (diff) | |
Extend raft replay with recover and later crash
Diffstat (limited to 'src/test')
| -rw-r--r-- | src/test/java/core/VSTaskManagerCrashRecoveryIntegrationTest.java | 153 | ||||
| -rw-r--r-- | src/test/java/simulator/builder/SimulationBuilderTest.java | 15 |
2 files changed, 168 insertions, 0 deletions
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; + } } |
