Files
JIMRI/java/test/jmri/managers/DefaultShutDownManagerTest.java
2026-06-17 14:00:51 +02:00

451 lines
15 KiB
Java

package jmri.managers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.util.concurrent.Callable;
import java.util.concurrent.*;
import java.util.Date;
import org.junit.Assert;
import org.junit.jupiter.api.*;
import jmri.InstanceManager;
import jmri.ShutDownManager;
import jmri.ShutDownTask;
import jmri.implementation.AbstractShutDownTask;
import jmri.util.JUnitAppender;
import jmri.util.JUnitUtil;
/**
*
* @author Paul Bender Copyright 2017
* @author Randall Wood Copyright 2020
*/
@Timeout(10)
public class DefaultShutDownManagerTest {
private ConcurrentMap<String, String> concurrentRuns = new ConcurrentHashMap<>();
private ConcurrentMap<String, String> concurrentEarlyRuns = new ConcurrentHashMap<>();
private int runs;
private DefaultShutDownManager dsdm;
@Test
public void testCTor() {
// remove the default shutdown hook to prevent crashes stopping tests
Runtime.getRuntime().removeShutdownHook(dsdm.shutdownHook);
Assert.assertNotNull("exists", dsdm);
}
@Test
public void testRegister_Task() {
Assert.assertEquals(0, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
ShutDownTask task = new AbstractShutDownTask("task") {
@Override
public void run() {
}
};
dsdm.register(task);
Assert.assertEquals(1, dsdm.getRunnables().size());
Assert.assertEquals(1, dsdm.getCallables().size());
dsdm.register(task);
Assert.assertEquals(1, dsdm.getRunnables().size());
Assert.assertEquals(1, dsdm.getCallables().size());
assertThatCode(() -> registerNullShutDownTask(dsdm)).isInstanceOf(NullPointerException.class);
}
@SuppressFBWarnings( value = "NP_NONNULL_PARAM_VIOLATION", justification = "passing null to non-null to check exception")
private void registerNullShutDownTask(DefaultShutDownManager d) {
d.register((ShutDownTask) null);
}
@Test
public void testDeregister_Task() {
Assert.assertEquals(0, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
ShutDownTask task = new AbstractShutDownTask("task") {
@Override
public void run() {
}
};
dsdm.register(task);
Assert.assertEquals(1, dsdm.getRunnables().size());
Assert.assertEquals(1, dsdm.getCallables().size());
Assert.assertTrue(dsdm.getCallables().contains(task));
dsdm.deregister(task);
Assert.assertEquals(0, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
assertThatCode(() -> dsdm.deregister((ShutDownTask) null)).doesNotThrowAnyException();
}
@Test
public void testRegister_Callable() {
Assert.assertEquals(0, dsdm.getCallables().size());
Assert.assertEquals(0, dsdm.getRunnables().size());
Callable<Boolean> task = () -> true;
dsdm.register(task);
Assert.assertEquals(1, dsdm.getCallables().size());
Assert.assertEquals(0, dsdm.getRunnables().size());
dsdm.register(task);
Assert.assertEquals(1, dsdm.getCallables().size());
Assert.assertEquals(0, dsdm.getRunnables().size());
assertThatCode(() -> registerNullCallable(dsdm) ).isInstanceOf(NullPointerException.class);
}
@SuppressFBWarnings( value = "NP_NONNULL_PARAM_VIOLATION", justification = "passing null to non-null to check exception")
private void registerNullCallable(DefaultShutDownManager d) {
d.register((Callable<Boolean>) null);
}
@Test
public void testDeregister_Callable() {
Assert.assertEquals(0, dsdm.getCallables().size());
Assert.assertEquals(0, dsdm.getRunnables().size());
Callable<Boolean> task = () -> true;
dsdm.register(task);
Assert.assertEquals(1, dsdm.getCallables().size());
Assert.assertEquals(0, dsdm.getRunnables().size());
Assert.assertTrue(dsdm.getCallables().contains(task));
dsdm.deregister(task);
Assert.assertEquals(0, dsdm.getCallables().size());
Assert.assertEquals(0, dsdm.getRunnables().size());
assertThatCode(() -> dsdm.deregister((Callable<Boolean>) null)).doesNotThrowAnyException();
}
@Test
@SuppressFBWarnings( value = "NP_NONNULL_PARAM_VIOLATION", justification = "passing null to non-null to check exception")
public void testRegister_Runnable() {
Assert.assertEquals(0, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
Runnable task = () -> {};
dsdm.register(task);
Assert.assertEquals(1, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
dsdm.register(task);
Assert.assertEquals(1, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
assertThatCode(() -> registerNullRunnable(dsdm) ).isInstanceOf(NullPointerException.class);
}
@SuppressFBWarnings( value = "NP_NONNULL_PARAM_VIOLATION", justification = "passing null to non-null to check exception")
private void registerNullRunnable(DefaultShutDownManager d) {
d.register((Runnable) null);
}
@Test
public void testDeregister_Runnable() {
Assert.assertEquals(0, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
Runnable task = () -> {};
dsdm.register(task);
Assert.assertEquals(1, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
Assert.assertTrue(dsdm.getRunnables().contains(task));
dsdm.deregister(task);
Assert.assertEquals(0, dsdm.getRunnables().size());
Assert.assertEquals(0, dsdm.getCallables().size());
assertThatCode(() -> dsdm.deregister((Runnable) null)).doesNotThrowAnyException();
}
@Test
public void testIsShuttingDown() {
Frame frame = null;
if (!GraphicsEnvironment.isHeadless()) {
frame = new Frame("Shutdown test frame");
}
Assert.assertFalse(dsdm.isShuttingDown());
dsdm.shutdown(0, false);
Assert.assertTrue(dsdm.isShuttingDown());
if (frame != null) {
JUnitUtil.dispose(frame);
}
}
@Test
public void testShutDownisInterruptedByShutDownTask() {
dsdm.register(new AbstractShutDownTask("test") {
@Override
public Boolean call() {
return false;
}
@Override
public void run() {
runs++;
}});
dsdm.shutdown(0, false);
assertThat(dsdm.isShuttingDown()).isFalse();
assertThat(runs).isEqualTo(0);
}
@Test
public void testShutDownisNotInterruptedByShutDownTask() {
dsdm.register(new CallTrueShutDownTask("testShutDownisNotInterruptedByShutDownTask"));
dsdm.shutdown(0, false);
assertThat(dsdm.isShuttingDown()).isTrue();
assertThat(runs).isEqualTo(1);
JUnitUtil.waitFor( () -> dsdm.isShutDownComplete(),"ShutDown Completed");
}
@Test
public void testShutDownisInterruptedByCallableFalse() {
dsdm.register(() -> runs++);
dsdm.register(() -> Boolean.FALSE);
dsdm.shutdown(0, false);
assertThat(dsdm.isShuttingDown()).isFalse();
assertThat(runs).isEqualTo(0);
}
@Test
public void testShutDownisInterruptedByCallableException() {
dsdm.register(() -> runs++);
dsdm.register(() -> {
throw new Exception();
});
dsdm.shutdown(0, false);
assertThat(dsdm.isShuttingDown()).isFalse();
assertThat(runs).isEqualTo(0);
JUnitUtil.waitFor(dsdm.tasksTimeOutMilliSec * 4);
JUnitAppender.assertErrorMessageStartsWith("Unable to stop");
}
@Test
public void testShutDownisNotInterruptedByCallable() {
dsdm.register(() -> runs++);
dsdm.register(() -> true);
dsdm.shutdown(0, false);
assertThat(dsdm.isShuttingDown()).isTrue();
assertThat(runs).isEqualTo(1);
JUnitUtil.waitFor( () -> dsdm.isShutDownComplete(),"ShutDown Completed");
}
@Test
public void testInstanceManagerCreates() {
assertThat(InstanceManager.getNullableDefault(ShutDownManager.class)).isNotNull();
}
@Test
public void testShutDownTaskTakesTooLong() {
dsdm.register(new TakesOneSecondShutDownTask("testShutDownTaskTakesTooLong"));
dsdm.shutdown(0, false);
Assertions.assertTrue(dsdm.isShuttingDown());
JUnitUtil.waitFor(() -> { return runs == 2; } , "Second runs++ call eventually triggered");
JUnitUtil.waitFor( () -> dsdm.isShutDownComplete(),"ShutDown Completed");
JUnitUtil.waitFor(dsdm.tasksTimeOutMilliSec * 4);
JUnitAppender.assertErrorMessageStartsWith("JMRI ShutDown - Main Tasks Task timed out:");
}
@Test
public void testEarlyShutDownTaskTakesTooLong() {
dsdm.register(new TakesOneSecondEarlyShutDownTask("testEarlyShutDownTaskTakesTooLong"));
dsdm.shutdown(0, false);
Assertions.assertTrue(dsdm.isShuttingDown());
JUnitUtil.waitFor(() -> { return runs == 2; } , "Second runs++ call eventually triggered");
JUnitUtil.waitFor( () -> dsdm.isShutDownComplete(),"ShutDown Completed");
JUnitUtil.waitFor(dsdm.tasksTimeOutMilliSec * 4);
JUnitAppender.assertErrorMessageStartsWith("JMRI ShutDown - Early Tasks Task timed out:");
}
@Test
public void testShutDownTaskThrowsException() {
dsdm.register(new ThrowsExceptionShutDownTask("testShutDownTaskThrowsException"));
dsdm.shutdown(0, false);
Assertions.assertTrue(dsdm.isShuttingDown());
Assertions.assertEquals( 1, runs, "run triggered");
JUnitUtil.waitFor( () -> dsdm.isShutDownComplete(),"ShutDown Completed");
JUnitUtil.waitFor(dsdm.tasksTimeOutMilliSec * 4);
JUnitAppender.assertErrorMessageStartsWith("JMRI ShutDown - Main Tasks Exception in task:");
}
@Test
public void testMoreThanEightTasks() {
concurrentRuns = new ConcurrentHashMap<>(41);
dsdm.tasksTimeOutMilliSec *= 2;
for ( int i=0; i<30; i++ ){
dsdm.register(new CallTrueShutDownTask("testMoreThanEightTasks"+i));
}
Assertions.assertEquals(30, dsdm.getRunnables().size());
dsdm.shutdown(0, false);
Assertions.assertTrue(dsdm.isShuttingDown());
// Assertions.assertEquals(30, runs); // fails often with a few missing
// JUnitUtil.waitFor times out every now and then, so we use concurrentRuns
Assertions.assertEquals(30, concurrentRuns.size(),"all runs triggered");
JUnitUtil.waitFor( () -> dsdm.isShutDownComplete(),"ShutDown Completed");
}
@Test
public void testManagerDoesNotUseFullTimeOutWhenComplete() {
Date start = new Date();
dsdm.register(new CallTrueShutDownTask("testManagerDoesNotUseFullTimeOutWhenComplete"));
dsdm.shutdown(0, false);
Assertions.assertTrue(dsdm.isShuttingDown());
JUnitUtil.waitFor(() -> runs == 1 , "run triggered");
long testTime = new Date().getTime() - start.getTime();
Assertions.assertTrue(testTime < dsdm.tasksTimeOutMilliSec*2,
"Completed before earlytimeout and mainTimeout");
JUnitUtil.waitFor( () -> dsdm.isShutDownComplete(),"ShutDown Completed");
}
@Test
public void testEarlyTasks() {
concurrentEarlyRuns = new ConcurrentHashMap<>(3);
concurrentRuns = new ConcurrentHashMap<>(3);
dsdm.tasksTimeOutMilliSec *= 2;
dsdm.register(new EarlyShutDownTask("testEarlyTasks"));
Thread t1 = new Thread(() -> {
dsdm.shutdown(0, false);
});
t1.setName("testEarlyTasksShutdown");
t1.start();
JUnitUtil.waitFor(() -> concurrentEarlyRuns.size() == 1 , "early run triggered");
Assertions.assertEquals(0, concurrentRuns.size(),"early run triggered before main run");
JUnitUtil.waitFor(() -> concurrentRuns.size() == 1 , "run triggered");
JUnitUtil.waitFor(()->{return !(t1.isAlive());}, "shutdown thread completed");
JUnitUtil.waitFor( () -> dsdm.isShutDownComplete(),"ShutDown Completed");
}
private class CallTrueShutDownTask extends AbstractShutDownTask {
CallTrueShutDownTask(String id){
super("Call True Task " + id );
}
@Override
public Boolean call() {
return true;
}
@Override
public void run() {
runs++;
concurrentRuns.putIfAbsent(this.getName(), this.getName());
}
}
private class ThrowsExceptionShutDownTask extends AbstractShutDownTask {
ThrowsExceptionShutDownTask(String id){
super("Throws NPE Task " + id );
}
@Override
public Boolean call() {
return true;
}
@Override
public void run() {
runs++;
throw new NullPointerException("ThrowsExceptionShutDownTask " + getName());
}
}
private class TakesOneSecondShutDownTask extends AbstractShutDownTask {
TakesOneSecondShutDownTask(String id){
super("Takes Too Long " + id );
}
@Override
public Boolean call() {
return true;
}
@Override
public void run() {
runs++;
JUnitUtil.waitFor(1000);
runs++;
}
}
private class TakesOneSecondEarlyShutDownTask extends AbstractShutDownTask {
TakesOneSecondEarlyShutDownTask(String id){
super("Early SDT Takes Too Long " + id );
}
@Override
public Boolean call() {
return true;
}
@Override
public void runEarly() {
runs++;
JUnitUtil.waitFor(1000);
runs++;
}
@Override
public void run() {
// does nothing
}
}
private class EarlyShutDownTask extends AbstractShutDownTask {
EarlyShutDownTask(String id){
super("EarlyShutDownTask " + id );
}
@Override
public Boolean call() {
return true;
}
@Override
public void runEarly() {
concurrentEarlyRuns.putIfAbsent(this.getName(), this.getName());
JUnitUtil.waitFor(100);
}
@Override
public void run() {
concurrentRuns.putIfAbsent(this.getName(), this.getName());
}
}
@BeforeEach
public void setUp() {
JUnitUtil.setUp();
JUnitUtil.waitFor(50);
dsdm = new DefaultShutDownManager();
dsdm.setBlockingShutdown(true);
dsdm.tasksTimeOutMilliSec = 200; // normal default 30000 msecs but this is a test
runs = 0;
InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).setEnableStoreCheck(false);
}
@AfterEach
public void tearDown() {
JUnitUtil.waitFor(50);
dsdm = null;
DefaultShutDownManager.setStaticShuttingDown(false);
JUnitUtil.tearDown();
}
// private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultShutDownManagerTest.class);
}