summaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-27 15:16:59 +0200
committerPaul Buetow <paul@buetow.org>2026-03-27 15:16:59 +0200
commitca9d7633d76871cb0ea00dd46af351daf8ef4895 (patch)
treec45ff231af84400587a365cfe1d1663b98078b35 /src/test
parent7088d292893e3e20324b04527e93de9b1c73f6aa (diff)
Extend raft replay with recover and later crash
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/core/VSTaskManagerCrashRecoveryIntegrationTest.java153
-rw-r--r--src/test/java/simulator/builder/SimulationBuilderTest.java15
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;
+ }
}