Files
2026-06-17 14:00:51 +02:00

1780 lines
74 KiB
Java

package jmri.util;
import static org.junit.jupiter.api.Assertions.fail;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import javax.annotation.CheckForNull;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.swing.AbstractButton;
import jmri.*;
import jmri.implementation.JmriConfigurationManager;
import jmri.jmrit.blockboss.BlockBossLogicProvider;
import jmri.jmrit.display.EditorFrameOperator;
import jmri.jmrit.display.EditorManager;
import jmri.jmrit.display.layoutEditor.LayoutBlockManager;
import jmri.jmrit.logix.OBlockManager;
import jmri.jmrit.logix.WarrantManager;
import jmri.jmrit.logixng.*;
import jmri.jmrit.logixng.implementation.DefaultLogixNGManager;
import jmri.jmrit.logixng.implementation.DefaultConditionalNGManager;
import jmri.jmrit.logixng.implementation.DefaultAnalogActionManager;
import jmri.jmrit.logixng.implementation.DefaultAnalogExpressionManager;
import jmri.jmrit.logixng.implementation.DefaultDigitalActionManager;
import jmri.jmrit.logixng.implementation.DefaultDigitalBooleanActionManager;
import jmri.jmrit.logixng.implementation.DefaultDigitalExpressionManager;
import jmri.jmrit.logixng.implementation.DefaultStringActionManager;
import jmri.jmrit.logixng.implementation.DefaultStringExpressionManager;
import jmri.jmrit.roster.RosterConfigManager;
import jmri.jmrix.ConnectionConfigManager;
import jmri.jmrix.debugthrottle.DebugThrottleManager;
import jmri.jmrix.internal.*;
import jmri.managers.*;
import jmri.profile.*;
import jmri.progdebugger.DebugProgrammerManager;
import jmri.util.gui.GuiLafPreferencesManager;
import jmri.util.managers.*;
import jmri.util.prefs.*;
import jmri.util.zeroconf.MockZeroConfServiceManager;
import jmri.util.zeroconf.ZeroConfServiceManager;
import org.slf4j.event.Level;
import org.junit.jupiter.api.Assertions;
import org.netbeans.jemmy.*;
import org.netbeans.jemmy.operators.*;
/**
* Common utility methods for working with JUnit.
* <p>
* To release the current thread and allow other listeners to execute: <code><pre>
* JUnitUtil.waitFor(int time);
* </pre></code> Note that this is not appropriate for Swing objects; you need
* to use Jemmy for that.
* <p>
* If you're using the InstanceManager, setUp() implementation should start
* with:
* <pre><code>
* JUnitUtil.setUp();
* JUnitUtil.initInternalTurnoutManager();
* JUnitUtil.initInternalLightManager();
* JUnitUtil.initInternalSensorManager();
* JUnitUtil.initDebugThrottleManager();
* </code></pre>
* <p>
* Your tearDown() should end with:
* <pre><code>
* JUnitUtil.tearDown();
* </code></pre>
* <p>
* Note that memory managers and some others are completely internal, and will
* be reset when you reset the instance manager.
* <p>
* Messages originating in this class are printed via System.err instead of
* being logged because this class doesn't assume logging has been initialized.
*
* @author Bob Jacobsen Copyright 2009, 2015, 2026
* @since 2.5.3
*/
public class JUnitUtil {
/**
* Standard time (in mSec) to wait when releasing
* a thread during a test.
* <p>
* The method releaseThread() is removed but this constant is still used
* by some tests when calling waitFor(int time).
* <p>
* Public in case modification is needed from a test or script.
*/
public static final int WAITFOR_DEFAULT_DELAY = 50;
/**
* Default standard time step (in mSec) when looping in a waitFor operation.
*/
protected static final int DEFAULT_WAITFOR_DELAY_STEP = 5;
/**
* Standard time step (in mSec) when looping in a waitFor operation.
* <p>
* Public in case modification is needed from a test or script.
* This value is always reset to {@value #DEFAULT_WAITFOR_DELAY_STEP}
* during setUp().
*/
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "MS_CANNOT_BE_FINAL",
justification = "value reset during setUp() ")
public static int WAITFOR_DELAY_STEP = DEFAULT_WAITFOR_DELAY_STEP;
/**
* Default maximum time to wait before failing a waitFor operation.
* <p>
* The default value is really long, but that only matters when the test
* is failing anyway, and some of the LayoutEditor/SignalMastLogic tests
* are slow. But too long will cause CI jobs to time out before this logs
* the error....
*/
protected static final int DEFAULT_WAITFOR_MAX_DELAY = 10000;
/**
* Maximum time to wait before failing a waitFor operation.
* <p>
* Public in case modification is needed from a test or script.
* This value is always reset to {@value #DEFAULT_WAITFOR_MAX_DELAY} during setUp().
*/
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "MS_CANNOT_BE_FINAL",
justification = "value reset during setUp() ")
public static int WAITFOR_MAX_DELAY = DEFAULT_WAITFOR_MAX_DELAY;
/**
* When true, prints each setUp method to help identify which tests include a failure.
* When checkSetUpTearDownSequence is also true, this also prints on execution of tearDown.
* <p>
* Set from the jmri.util.JUnitUtil.printSetUpTearDownNames environment variable.
*/
static boolean printSetUpTearDownNames = Boolean.getBoolean("jmri.util.JUnitUtil.printSetUpTearDownNames"); // false unless set true
/**
* When true, checks that calls to setUp and tearDown properly alterante, printing an
* error message with context information on System.err if inconsistent calls are observed.
* <p>
* Set from the jmri.util.JUnitUtil.checkSetUpTearDownSequence environment variable.
*/
static boolean checkSetUpTearDownSequence = Boolean.getBoolean("jmri.util.JUnitUtil.checkSetUpTearDownSequence"); // false unless set true
/**
* Adds extensive error information to the output of checkSetUpTearDownSequence.
* Note: The context checking and storage required for this takes a lot of time.
* <p>
* Set from the jmri.util.JUnitUtil.checkSequenceDumpsStack environment variable.
*/
static boolean checkSequenceDumpsStack = Boolean.getBoolean("jmri.util.JUnitUtil.checkSequenceDumpsStack"); // false unless set true
/**
* If true, will cause the checkSetUpTearDownSequence check to
* fail the test in addition to logging.
* <p>
* Set from the jmri.util.JUnitUtil.checkSequenceFailsTest environment variable.
*/
static boolean checkSequenceFailsTest = Boolean.getBoolean("jmri.util.JUnitUtil.checkSequenceFailsTest"); // false unless set true
/**
* Announce any threads left behind after a test calls {@link #tearDown}
* <p>
* Set from the jmri.util.JUnitUtil.checkRemnantThreads environment variable.
*/
// static boolean checkRemnantThreads = true;
static boolean checkRemnantThreads = Boolean.getBoolean("jmri.util.JUnitUtil.checkRemnantThreads"); // false unless set true
/**
* Fail Test if any threads left behind after a test calls {@link #tearDown}
* <p>
* Set from the jmri.util.JUnitUtil.failRemnantThreads environment variable.
*/
// static boolean failRemnantThreads = true;
static boolean failRemnantThreads = Boolean.getBoolean("jmri.util.JUnitUtil.failRemnantThreads"); // false unless set true
/**
* Kill any threads left behind after a test calls {@link #tearDown}
* <p>
* Set from the jmri.util.JUnitUtil.killRemnantThreads environment variable
*/
static boolean killRemnantThreads = Boolean.getBoolean("jmri.util.JUnitUtil.killRemnantThreads"); // false unless set true
/**
* Check for tests that take an excessive time
* <p>
* Set from the jmri.util.JUnitUtil.checkTestDuration environment variable.
*/
static boolean checkTestDuration = Boolean.getBoolean("jmri.util.JUnitUtil.checkTestDuration"); // false unless set true
static long checkTestDurationMax = Long.getLong("jmri.util.JUnitUtil.checkTestDurationMax", 5000); // milliseconds
static long checkTestDurationStartTime = 0; // working value
private static boolean didSetUp = false; // If true, last saw setUp, waiting tearDown normally
private static boolean didTearDown = true; // If true, last saw tearDown, waiting setUp normally
private static String lastSetUpClassName = "<unknown>";
private static String lastSetUpThreadName = "<unknown>";
private static StackTraceElement[] lastSetUpStackTrace = new StackTraceElement[0];
private static String lastTearDownClassName = "<unknown>";
private static String lastTearDownThreadName = "<unknown>";
private static StackTraceElement[] lastTearDownStackTrace = new StackTraceElement[0];
private static boolean isLoggingInitialized = false;
private static String initPrefsDir = null;
/**
* JMRI standard setUp for tests that mock the InstanceManager.
* This should be the first line in the {@code @BeforeEach}
* annotated method if the tests mock the InstanceManager.
* <p>
* One or the other of {@link #setUp()} or {@link #setUpLoggingAndCommonProperties()} must
* be present in the {@code @BeforeEach} routine.
* <p>
*/
public static void setUpLoggingAndCommonProperties() {
if (!isLoggingInitialized) {
// init logging if needed
isLoggingInitialized = true;
String filename = System.getProperty("jmri.log4jconfigfilename", "tests_lcf.xml");
TestingLoggerConfiguration.initLogging(filename);
}
// need to do this each time
try {
JUnitAppender.startLogging();
// reset warn _only_ once logic to make tests repeatable
JUnitLoggingUtil.restartWarnOnce();
// ensure logging of deprecated method calls;
// individual tests can turn off as needed
JUnitLoggingUtil.setDeprecatedLogging(true);
} catch (Throwable e) {
System.err.println("Could not start JUnitAppender, but test continues:\n" + e);
}
// clear the backlog and reset the UnexpectedMessageFlags so that
// errors from a previous test do not interfere with the current test.
JUnitAppender.clearBacklog();
JUnitAppender.resetUnexpectedMessageFlags(Level.INFO);
// do not set the UncaughtExceptionHandler while unit testing
// individual tests can explicitly set it after calling this method
Thread.setDefaultUncaughtExceptionHandler(null);
// make sure the jmri.prefsdir property match the property passed
// to the tests.
if (initPrefsDir == null) {
initPrefsDir = System.getProperty("jmri.prefsdir", "./temp");
}
System.setProperty("jmri.prefsdir", initPrefsDir);
// silence the Jemmy GUI unit testing framework
JUnitUtil.silenceGUITestOutput();
// ideally this would be resetWindows(false, true) to force an error if an earlier
// test left a window open, but different platforms seem to have just
// enough differences that this is, for now, turned off
resetWindows(false, false, "before");
// Log and/or check the use of setUp and tearDown
if (checkSetUpTearDownSequence || printSetUpTearDownNames) {
lastSetUpClassName = getTestClassName();
if (printSetUpTearDownNames) System.err.println(">> Starting test in "+lastSetUpClassName);
if ( checkSetUpTearDownSequence) {
if (checkSequenceDumpsStack) lastSetUpThreadName = Thread.currentThread().getName();
if (didSetUp || ! didTearDown) {
System.err.println(" "+getTestClassName()+".setUp on thread "+lastSetUpThreadName+" unexpectedly found setUp="+didSetUp+" tearDown="+didTearDown+"; last setUp was in "+lastSetUpClassName+" thread "+lastSetUpThreadName);
if (checkSequenceDumpsStack) {
System.err.println("---- This stack ------");
Thread.dumpStack();
System.err.println("---- Last setUp stack ------");
for (StackTraceElement e : lastSetUpStackTrace) System.err.println(" at " + e);
System.err.println("---- Last tearDown stack ------");
for (StackTraceElement e : lastTearDownStackTrace) System.err.println(" at " + e);
System.err.println("----------------------");
}
if (checkSequenceFailsTest) {
Assertions.fail("setUp and tearDown did not match");
}
}
didTearDown = false;
didSetUp = true;
if (checkSequenceDumpsStack) lastSetUpStackTrace = Thread.currentThread().getStackTrace();
}
}
// checking time?
if (checkTestDuration) {
checkTestDurationStartTime = System.currentTimeMillis();
}
}
/**
* JMRI standard setUp for tests.
* This should be the first line in the {@code @BeforeEach}
* annotated method if the tests do not mock the InstanceManager.
* <p>
* One or the other of {@link #setUp()} or {@link #setUpLoggingAndCommonProperties()} must
* be present in the {@code @BeforeEach} routine.
* <p>
* Calls {@link #setUpLoggingAndCommonProperties()}, {@link #resetInstanceManager()}
* and sets the jmri.configurexml.ShutdownPreferences setEnableStoreCheck to false.
*/
public static void setUp() {
WAITFOR_DELAY_STEP = DEFAULT_WAITFOR_DELAY_STEP;
WAITFOR_MAX_DELAY = DEFAULT_WAITFOR_MAX_DELAY;
// all the setup for a MockInstanceManager applies
setUpLoggingAndCommonProperties();
// Do a minimal amount of de-novo setup
resetInstanceManager();
InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).setEnableStoreCheck(false);
}
/**
* Silently remove any AbstractTurnout threads that are still running.
* A bit expensive, so only used when needed.
*/
public static void clearTurnoutThreads(){
removeMatchingThreads("setCommandedStateAtInterval"); // must stay consistent with AbstractTurnout
}
/**
* Silently remove any DefaultRoute threads that are still running.
* A bit expensive, so only used when needed.
*/
public static void clearRouteThreads(){
removeMatchingThreads("setRoute"); // must stay consistent with DefaultRoute
}
/**
* Silently remove any blockboss/Simple Signal Logic threads that are still running.
* A bit expensive, so only used when needed.
*/
public static void clearBlockBossLogicThreads(){
removeMatchingThreads("BlockBossLogic");
}
/**
* Utility to remove any threads with a matching name
* @param nameContains The thread name to search
*/
public static void removeMatchingThreads(String nameContains) {
ThreadGroup main = Thread.currentThread().getThreadGroup();
while (main.getParent() != null ) {main = main.getParent(); }
Thread[] list = new Thread[main.activeCount()+2]; // space on end
int max = main.enumerate(list);
for (int i = 0; i<max; i++) {
Thread t = list[i];
if (t.getState() == Thread.State.TERMINATED) { // going away, just not cleaned up yet
continue;
}
String name = t.getName();
if (name.contains(nameContains)) {
killThread(t);
}
}
}
@SuppressWarnings("deprecation") // Thread.stop()
static void killThread(Thread t) {
t.interrupt();
try {
t.join(100); // give it a bit of time to end
if (t.getState() != Thread.State.TERMINATED) {
log.warn(" Thread {} did not terminate", t.getName());
}
} catch (IllegalMonitorStateException | IllegalStateException | InterruptedException e) {
log.error("While interrupting thread {}:", t.getName(), e);
}
}
/**
* Teardown from tests. This should be the last line in the {@code @After}
* annotated method.
*/
public static void tearDown() {
// Stop all LogixNG threads
jmri.jmrit.logixng.util.LogixNG_Thread.stopAllLogixNGThreads();
// check that no LogixNG threads are still running
jmri.jmrit.logixng.util.LogixNG_Thread.assertLogixNGThreadNotRunning();
// checking time?
if (checkTestDuration) {
long duration = System.currentTimeMillis() - checkTestDurationStartTime;
if (duration > checkTestDurationMax) {
// test too long, log that
System.err.println("Test in "+getTestClassName()+" duration "+duration+" ms exceeded limit "+checkTestDurationMax);
}
}
// Log and/or check the use of setUp and tearDown
if (checkSetUpTearDownSequence || printSetUpTearDownNames) {
lastTearDownClassName = getTestClassName();
if (checkSetUpTearDownSequence) {
if (checkSequenceDumpsStack) lastTearDownThreadName = Thread.currentThread().getName();
if (! didSetUp || didTearDown) {
System.err.println(" "+getTestClassName()+".tearDown on thread "+lastTearDownThreadName+" unexpectedly found setUp="+didSetUp+" tearDown="+didTearDown+"; last tearDown was in "+lastTearDownClassName+" thread "+lastTearDownThreadName);
if (checkSequenceDumpsStack) {
System.err.println("---- This stack ------");
Thread.dumpStack();
System.err.println("---- Last setUp stack ------");
for (StackTraceElement e : lastSetUpStackTrace) System.err.println(" at " + e);
System.err.println("---- Last tearDown stack ------");
for (StackTraceElement e : lastTearDownStackTrace) System.err.println(" at " + e);
System.err.println("----------------------");
}
}
didSetUp = false;
didTearDown = true;
if (checkSequenceDumpsStack) lastTearDownStackTrace = Thread.currentThread().getStackTrace();
}
// To save time & space, only print end when doing full check
if (printSetUpTearDownNames && checkSetUpTearDownSequence) System.err.println("<< Ending test in "+lastTearDownClassName);
}
// ideally this would be resetWindows(false, true) to force an error if an earlier
// test left a window open, but different platforms seem to have just
// enough differences that this is, for now, turned off
resetWindows(false, false, "after");
// Check final status of logging in the test just completed
JUnitAppender.end();
Level severity = Level.ERROR; // level at or above which we'll complain
boolean unexpectedMessageSeen = JUnitAppender.unexpectedMessageSeen(severity);
String unexpectedMessageContent = JUnitAppender.unexpectedMessageContent(severity);
JUnitAppender.verifyNoBacklog();
JUnitAppender.resetUnexpectedMessageFlags(severity);
Assertions.assertFalse( unexpectedMessageSeen,
() -> "Unexpected "+severity+" or higher messages emitted: \""+unexpectedMessageContent+"\"");
// check for hanging shutdown tasks - after test for ERROR so it can complain
checkShutDownManager();
// Optionally, handle any threads left running
if (checkRemnantThreads || killRemnantThreads || failRemnantThreads) {
handleThreads();
}
// Optionally, print whatever is on the Swing queue to see what's keeping things alive
//Object entry = java.awt.Toolkit.getDefaultToolkit().getSystemEventQueue().peekEvent();
//if (entry != null) System.err.println("entry: "+entry);
// Optionally, check that the Swing queue is idle
//new org.netbeans.jemmy.QueueTool().waitEmpty(250);
}
/**
* Wait for a specific condition to be true, without having to wait longer.
* <p>
* To be used in tests, will do an assert if the total delay is longer than
* WAITFOR_MAX_DELAY
* <p>
* Typical use:
* <code>JUnitUtil.waitFor(()->{return replyVariable != null;},"reply not received");</code>
*
* @param condition condition being waited for
* @param name name of condition being waited for; will appear in
* Assertions.fail if condition not true fast enough
*/
public static void waitFor( @Nonnull ReleaseUntil condition, @Nonnull String name) {
waitFor( condition, () -> name);
}
/**
* Wait for a specific condition to be true, without having to wait longer.
* <p>
* To be used in tests, will fail test if the total delay is longer than
* WAITFOR_MAX_DELAY.
* <p>
* The messageSupplier is not evaluated unless there is a test failure so
* can include expensive method calls and string joins without penalty.
* <p>
* Typical use:
* <code>JUnitUtil.waitFor( () -> { return replyVariable != null; },
* () -> "replyVariable still null: " + computationallyExpensiveCall() + " or multiple Strings" );</code>
* @param condition condition being waited for.
* @param messageSupplier Failure text supplier.
*/
@SuppressFBWarnings("REC_CATCH_EXCEPTION")
public static void waitFor( @Nonnull ReleaseUntil condition , @Nonnull Supplier<String> messageSupplier) {
if (javax.swing.SwingUtilities.isEventDispatchThread()) {
log.error("Cannot use waitFor on Swing thread", new Exception());
return;
}
int delay = 0;
try {
while (delay < WAITFOR_MAX_DELAY) {
try {
if (condition.ready()) {
return;
}
} catch(Exception ex) {
fail("Exception while processing condition for \"" + messageSupplier.get() + "\" ", ex);
}
int priority = Thread.currentThread().getPriority();
try {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
Thread.sleep(WAITFOR_DELAY_STEP);
delay += WAITFOR_DELAY_STEP;
} catch (InterruptedException e) {
fail("failed due to InterruptedException", e);
} finally {
Thread.currentThread().setPriority(priority);
}
}
fail("\"" + messageSupplier.get() + "\" did not occur in time");
} catch (Exception ex) {
fail("Exception while waiting for \"" + messageSupplier.get() + "\" ", ex);
}
}
/**
* Wait for a specific condition to be true, without having to wait longer
* <p>
* To be used in assumptions, will return false if the total delay is longer
* than WAITFOR_MAX_DELAY
* <p>
* Typical use:
* <code>Assumptions.assumeTrue("reply not received", JUnitUtil.waitFor(()->{return replyVariable != null;}));</code>
*
* @param condition condition to wait for
* @return true if condition is met before WAITFOR_MAX_DELAY, false
* otherwise
*/
@CheckReturnValue
public static boolean waitFor(ReleaseUntil condition) {
if (javax.swing.SwingUtilities.isEventDispatchThread()) {
log.error("Cannot use waitFor on Swing thread", new Exception());
return false;
}
int delay = 0;
try {
while (delay < WAITFOR_MAX_DELAY) {
if (condition.ready()) {
return true;
}
int priority = Thread.currentThread().getPriority();
try {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
Thread.sleep(WAITFOR_DELAY_STEP);
delay += WAITFOR_DELAY_STEP;
} catch (InterruptedException e) {
return false;
} finally {
Thread.currentThread().setPriority(priority);
}
}
return false;
} catch (Exception ex) {
log.error("Exception in waitFor condition.", ex);
return false;
}
}
/**
* Wait for a specific amount of time
* <p>
* It's better to wait for a condition, but if you can't find a condition,
* this will have to do.
* <p>
*
* @param msec Delay in milliseconds
*/
public static void waitFor(int msec) {
if (javax.swing.SwingUtilities.isEventDispatchThread()) {
log.error("Cannot use waitFor on Swing thread", new Exception());
return;
}
int delay = 0;
try {
while (delay < msec) {
int priority = Thread.currentThread().getPriority();
try {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
Thread.sleep(WAITFOR_DELAY_STEP);
delay += WAITFOR_DELAY_STEP;
} catch (InterruptedException e) {
return;
} finally {
Thread.currentThread().setPriority(priority);
}
}
} catch (Exception ex) {
log.error("Exception in waitFor condition.", ex);
}
}
/**
* Wait for a specific condition to be true, without having to wait longer
* <p>
* To be used in tests, will do an assert if the total delay is longer than
* 1 second
* <p>
* Typical use:
* <code>JUnitUtil.fasterWaitFor(()->{return replyVariable != null;},"reply not received")</code>
*
* @param condition condition being waited for
* @param name name of condition being waited for; will appear in
* Assertions.fail if condition not true fast enough
*/
@SuppressFBWarnings("REC_CATCH_EXCEPTION")
public static void fasterWaitFor(ReleaseUntil condition, String name) {
if (javax.swing.SwingUtilities.isEventDispatchThread()) {
log.error("Cannot use waitFor on Swing thread", new Exception());
return;
}
int delay = 0;
try {
while (delay < 1000) {
try {
if (condition.ready()) {
return;
}
} catch(Exception ex) {
Assertions.fail("Exception while processing condition for \"" + name + "\" ", ex);
}
int priority = Thread.currentThread().getPriority();
try {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
Thread.sleep(5);
delay += 5;
} catch (InterruptedException e) {
Assertions.fail("failed due to InterruptedException", e);
} finally {
Thread.currentThread().setPriority(priority);
}
}
Assertions.fail("\"" + name + "\" did not occur in time");
} catch (Exception ex) {
Assertions.fail("Exception while waiting for \"" + name + "\" ", ex);
}
}
/**
* Wait at most 1 second for a specific condition to be true, without having to wait longer
* <p>
* To be used in assumptions, will return false if the total delay is longer
* than 1000 milliseconds.
* <p>
* Typical use:
* <code>Assume.assumeTrue("reply not received", JUnitUtil.fasterWaitForTrue(()->{return replyVariable != null;}));</code>
*
* @param condition condition to wait for
* @return true if condition is met before 1 second, false
* otherwise
*/
@CheckReturnValue
public static boolean fasterWaitFor(ReleaseUntil condition) {
if (javax.swing.SwingUtilities.isEventDispatchThread()) {
log.error("Cannot use waitFor on Swing thread", new Exception());
return false;
}
int delay = 0;
try {
while (delay < 1000) {
if (condition.ready()) {
return true;
}
int priority = Thread.currentThread().getPriority();
try {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
Thread.sleep(5);
delay += 5;
} catch (InterruptedException e) {
return false;
} finally {
Thread.currentThread().setPriority(priority);
}
}
return false;
} catch (Exception ex) {
log.error("Exception in waitFor condition.", ex);
return false;
}
}
/**
* Reset the user files path in the default
* {@link jmri.util.FileUtilSupport} object (used by
* {@link jmri.util.FileUtil}) to the default settings/user files path for
* tests of {@code git-working-copy/temp}.
*/
public static void resetFileUtilSupport() {
FileUtilSupport.resetInstance();
FileUtilSupport.getDefault().setUserFilesPath(
ProfileManager.getDefault().getActiveProfile(), FileUtil.getPreferencesPath());
}
public static interface ReleaseUntil {
public boolean ready() throws Exception;
}
/**
* Set a NamedBean (Turnout, Sensor, SignalHead, ...) to a specific value in
* a thread-safe way.
* <p>
* You can't assume that all the consequences of that setting will have
* propagated through when this returns; those might take a long time. But
* the set operation itself will be complete.
*
* @param bean the bean
* @param state the desired state
*/
public static void setBeanState(NamedBean bean, int state) {
try {
javax.swing.SwingUtilities.invokeAndWait(
() -> {
try {
bean.setState(state);
} catch (JmriException e) {
log.error("Threw exception while setting state: ", e);
}
}
);
} catch (InterruptedException e) {
log.warn("Interrupted while setting state: ", e);
} catch (InvocationTargetException e) {
log.warn("Failed during invocation while setting state: ", e);
}
}
/**
* Set a NamedBean (Turnout, Sensor, SignalHead, ...) to a specific value in
* a thread-safe way, including waiting for the state to appear.
* <p>
* You can't assume that all the consequences of that setting will have
* propagated through when this returns; those might take a long time. But
* the set operation itself will be complete.
*
* @param bean the bean
* @param state the desired state
*/
public static void setBeanStateAndWait(NamedBean bean, int state) {
setBeanState(bean, state);
JUnitUtil.waitFor(() -> {
return state == bean.getState();
}, "setAndWait " + bean.getSystemName() + ": " + state);
}
/**
* Reset the Instance Manager.
* Clears all instances from the static InstanceManager.
* <p>
* Ensures the auto-default UserPreferencesManager is not created
* by installing a test one.
* <p>
* Sets the jmri.configurexml.ShutdownPreferences setEnableStoreCheck to false.
*/
public static void resetInstanceManager() {
// clear all instances from the static InstanceManager
InstanceManager.getDefault().clearAll();
// ensure the auto-default UserPreferencesManager is not created by installing a test one
InstanceManager.setDefault(UserPreferencesManager.class, new TestUserPreferencesManager());
InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).setEnableStoreCheck(false);
}
public static void resetTurnoutOperationManager() {
InstanceManager.reset(TurnoutOperationManager.class);
InstanceManager.getDefault(TurnoutOperationManager.class); // force creation
}
public static void initConfigureManager() {
InstanceManager.setDefault(ConfigureManager.class, new JmriConfigurationManager());
}
public static void initDefaultUserMessagePreferences() {
// remove the existing user preferences manager (if present)
InstanceManager.reset(UserPreferencesManager.class);
// create a test user preferences manager
InstanceManager.setDefault(UserPreferencesManager.class, new TestUserPreferencesManager());
}
public static void initInternalTurnoutManager() {
// now done automatically by InstanceManager's autoinit
InstanceManager.turnoutManagerInstance();
}
public static void initInternalLightManager() {
// now done automatically by InstanceManager's autoinit
InstanceManager.lightManagerInstance();
}
public static void initInternalSensorManager() {
// now done automatically by InstanceManager's autoinit
InstanceManager.sensorManagerInstance();
InternalSensorManager.setDefaultStateForNewSensors(jmri.Sensor.UNKNOWN);
}
public static void initRouteManager() {
// routes provide sensors, so ensure the sensor manager is initialized
// routes need turnouts, so ensure the turnout manager is initialized
JUnitUtil.initInternalSensorManager();
JUnitUtil.initInternalTurnoutManager();
// now done automatically by InstanceManager's autoinit
InstanceManager.getDefault(RouteManager.class);
}
public static void initMemoryManager() {
MemoryManager m = new DefaultMemoryManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class));
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m, jmri.Manager.MEMORIES);
}
}
public static void initReporterManager() {
ReporterManager m = new InternalReporterManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class));
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m, jmri.Manager.REPORTERS);
}
}
public static void initOBlockManager() {
OBlockManager b = new OBlockManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(b, jmri.Manager.OBLOCKS);
}
}
public static void deregisterBlockManagerShutdownTask() {
if (! InstanceManager.isInitialized(ShutDownManager.class)) return;
if (! InstanceManager.isInitialized(BlockManager.class)) return;
InstanceManager
.getDefault(ShutDownManager.class)
.deregister(InstanceManager.getDefault(BlockManager.class).shutDownTask);
}
public static void initWarrantManager() {
WarrantManager w = new WarrantManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(w, jmri.Manager.WARRANTS);
}
}
public static void initSignalMastLogicManager() {
SignalMastLogicManager w = new DefaultSignalMastLogicManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class));
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(w, jmri.Manager.SIGNALMASTLOGICS);
}
}
public static void initLayoutBlockManager() {
LayoutBlockManager w = new LayoutBlockManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(w, jmri.Manager.LAYOUTBLOCKS);
}
}
public static void initSectionManager() {
jmri.SectionManager w = new jmri.managers.DefaultSectionManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(w, jmri.Manager.SECTIONS);
}
}
public static void initInternalSignalHeadManager() {
SignalHeadManager m = new AbstractSignalHeadManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class));
InstanceManager.setDefault(SignalHeadManager.class, m);
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m, jmri.Manager.SIGNALHEADS);
}
}
public static void initDefaultSignalMastManager() {
InstanceManager.setDefault(SignalMastManager.class, new DefaultSignalMastManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class)));
}
public static void initDebugCommandStation() {
jmri.CommandStation cs = new jmri.CommandStation() {
@Override
public boolean sendPacket(@Nonnull byte[] packet, int repeats) {
return true;
}
@Override
public String getUserName() {
return "testCS";
}
@Override
public String getSystemPrefix() {
return "I";
}
};
InstanceManager.store(cs, jmri.CommandStation.class);
}
/**
* Creates a new DebugThrottleManager using the default
* InternalSystemConnectionMemo.
* Stores to both InstanceManager and System Connection.
*/
public static void initDebugThrottleManager() {
var memo = InstanceManager.getDefault(InternalSystemConnectionMemo.class);
initDebugThrottleManager(memo);
}
/**
* Creates a new DebugThrottleManager using the supplied SystemConnectionMemo.
* Stores to both InstanceManager and System Connection.
* @param memo the system connection to use.
*/
public static void initDebugThrottleManager(@Nonnull jmri.jmrix.DefaultSystemConnectionMemo memo) {
ThrottleManager m = new DebugThrottleManager(memo);
memo.store(m, ThrottleManager.class);
InstanceManager.store(m, ThrottleManager.class);
}
public static void initDebugProgrammerManager() {
DebugProgrammerManager m = new DebugProgrammerManager();
InstanceManager.store(m, AddressedProgrammerManager.class);
InstanceManager.store(m, GlobalProgrammerManager.class);
}
public static void initDebugPowerManager() {
InstanceManager.setDefault(PowerManager.class, new PowerManagerScaffold());
}
/**
* Initialize an {@link IdTagManager} that does not use persistent storage.
* If needing an IdTagManager that does use persistent storage use
* {@code InstanceManager.setDefault(IdTagManager.class, new DefaultIdTagManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class)));}
* to initialize an IdTagManager in the {@code @Before} annotated method of
* the test class or allow the {@link DefaultIdTagManager} to be
* automatically initialized when needed.
*/
public static void initIdTagManager() {
InstanceManager.reset(IdTagManager.class);
InstanceManager.setDefault(IdTagManager.class,
new DefaultIdTagManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class)) {
@Override
public void writeIdTagDetails() {
// do not actually write tags
this.dirty = false;
}
@Override
public void readIdTagDetails() {
// do not actually read tags
this.dirty = false;
}
@Override
protected void initShutdownTask(){
//don't even register the shutdownTask
}
});
}
public static void initRailComManager() {
InstanceManager.reset(jmri.RailComManager.class);
InstanceManager.store(new DefaultRailComManager(), jmri.RailComManager.class);
}
public static void initLogixManager() {
LogixManager m = new DefaultLogixManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class));
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m, jmri.Manager.LOGIXS);
}
}
public static void initConditionalManager() {
ConditionalManager m = new DefaultConditionalManager(InstanceManager.getDefault(InternalSystemConnectionMemo.class));
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m, jmri.Manager.CONDITIONALS);
}
}
public static void initInternalTurnoutManagerThrowException() {
InstanceManager.setDefault(TurnoutManager.class, new TurnoutManagerThrowExceptionScaffold());
}
public static void initLogixNGManager() {
initLogixNGManager(true);
}
public static void initLogixNGManager(boolean activate) {
LogixNG_Manager m1 = new DefaultLogixNGManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m1, jmri.Manager.LOGIXNGS);
}
InstanceManager.setDefault(LogixNG_Manager.class, m1);
ConditionalNG_Manager m2 = new DefaultConditionalNGManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m2, jmri.Manager.LOGIXNG_CONDITIONALNGS);
}
InstanceManager.setDefault(ConditionalNG_Manager.class, m2);
AnalogActionManager m3 = new DefaultAnalogActionManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m3, jmri.Manager.LOGIXNG_ANALOG_ACTIONS);
}
InstanceManager.setDefault(AnalogActionManager.class, m3);
AnalogExpressionManager m4 = new DefaultAnalogExpressionManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m4, jmri.Manager.LOGIXNG_ANALOG_EXPRESSIONS);
}
InstanceManager.setDefault(AnalogExpressionManager.class, m4);
DigitalActionManager m5 = new DefaultDigitalActionManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m5, jmri.Manager.LOGIXNG_DIGITAL_ACTIONS);
}
InstanceManager.setDefault(DigitalActionManager.class, m5);
DigitalBooleanActionManager m6 = new DefaultDigitalBooleanActionManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m6, jmri.Manager.LOGIXNG_DIGITAL_BOOLEAN_ACTIONS);
}
InstanceManager.setDefault(DigitalBooleanActionManager.class, m6);
DigitalExpressionManager m7 = new DefaultDigitalExpressionManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m7, jmri.Manager.LOGIXNG_DIGITAL_EXPRESSIONS);
}
InstanceManager.setDefault(DigitalExpressionManager.class, m7);
StringActionManager m8 = new DefaultStringActionManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m8, jmri.Manager.LOGIXNG_STRING_ACTIONS);
}
InstanceManager.setDefault(StringActionManager.class, m8);
StringExpressionManager m9 = new DefaultStringExpressionManager();
if (InstanceManager.getNullableDefault(ConfigureManager.class) != null) {
InstanceManager.getDefault(ConfigureManager.class).registerConfig(m9, jmri.Manager.LOGIXNG_STRING_EXPRESSIONS);
}
InstanceManager.setDefault(StringExpressionManager.class, m9);
jmri.jmrit.logixng.NamedBeanType.reset();
jmri.jmrit.logixng.actions.CommonManager.reset();
if (activate) m1.activateAllLogixNGs(false, false);
}
public static void initInternalSensorManagerThrowException() {
InstanceManager.setDefault(SensorManager.class, new SensorManagerThrowExceptionScaffold());
}
public static void initLightManagerThrowException() {
InstanceManager.setDefault(LightManager.class, new InternalLightManagerThrowExceptionScaffold());
}
public static void initMemoryManagerThrowException() {
InstanceManager.setDefault(MemoryManager.class, new MemoryManagerThrowExceptionScaffold());
}
public static void initSignalHeadManagerThrowException() {
InstanceManager.setDefault(SignalHeadManager.class, new SignalHeadManagerThrowExceptionScaffold());
}
public static void initSignalMastManagerThrowException() {
InstanceManager.setDefault(SignalMastManager.class, new SignalMastManagerThrowExceptionScaffold());
}
public static void initWarrantManagerThrowException() {
InstanceManager.setDefault(WarrantManager.class, new WarrantManagerThrowExceptionScaffold());
}
public static void initOBlockManagerThrowException() {
InstanceManager.setDefault(OBlockManager.class, new OBlockManagerThrowExceptionScaffold());
}
public static void initRouteManagerThrowException() {
InstanceManager.setDefault(RouteManager.class, new RouteManagerThrowExceptionScaffold());
}
/**
* Initialize a {@link jmri.util.zeroconf.MockZeroConfServiceManager} after
* ensuring that any existing
* {@link jmri.util.zeroconf.ZeroConfServiceManager} (real or mocked) has
* stopped all services it is managing.
*/
public static void initZeroConfServiceManager() {
Assertions.assertTrue(JUnitUtil.resetZeroConfServiceManager());
InstanceManager.setDefault(ZeroConfServiceManager.class, new MockZeroConfServiceManager());
}
/**
* Ensure that any existing
* {@link jmri.util.zeroconf.ZeroConfServiceManager} (real or mocked) has
* stopped all services it is managing.
* @return true when complete.
*/
@CheckReturnValue
public static boolean resetZeroConfServiceManager() {
if (! InstanceManager.containsDefault(ZeroConfServiceManager.class)) {
return true; // not present, don't create one by asking for it.
}
ZeroConfServiceManager manager = InstanceManager.getDefault(ZeroConfServiceManager.class);
manager.stopAll();
waitFor( () -> manager.allServices().isEmpty(), "Stopping all ZeroConf Services");
manager.dispose();
var threads = Thread.getAllStackTraces().keySet();
for (Thread t : threads) {
if (t.getName().startsWith(ZeroConfServiceManager.DNS_CLOSE_THREAD_NAME)) {
waitThreadTerminated(t);
}
}
return true;
}
/**
* End any running BlockBossLogic (Simple Signal Logic) objects
*/
public static void clearBlockBossLogic() {
if(InstanceManager.containsDefault(BlockBossLogicProvider.class)) {
InstanceManager.getDefault(BlockBossLogicProvider.class).dispose();
}
}
/**
* Leaves ShutDownManager, if any, in place,
* but removes its contents.
* <p>
* Instead of using this,
* it's better to have your test code remove _and_ _check_
* for specific items; this just suppresses output from the
* {@link #checkShutDownManager()} check down as part of the
* default end-of-test code.
*
* @see #checkShutDownManager()
*/
public static void clearShutDownManager() {
if (! InstanceManager.containsDefault(ShutDownManager.class)) return; // not present, stop (don't create)
ShutDownManager sm = InstanceManager.getDefault(jmri.ShutDownManager.class);
List<Callable<Boolean>> callables = sm.getCallables();
while (!callables.isEmpty()) {
Callable<Boolean> callable = callables.get(0);
sm.deregister(callable);
callables = sm.getCallables(); // avoid ConcurrentModificationException
}
List<Runnable> runnables = sm.getRunnables();
while (!runnables.isEmpty()) {
Runnable runnable = runnables.get(0);
sm.deregister(runnable);
runnables = sm.getRunnables(); // avoid ConcurrentModificationException
}
}
/**
* Fails test if the {@link jmri.ShutDownManager} was not left empty.
* Normally run as part of the default end-of-test code.
* Considered a failure so that the individual test can be identified.
*
* @see #clearShutDownManager()
*/
public static void checkShutDownManager() {
if (! InstanceManager.containsDefault(ShutDownManager.class)) {
return; // not present, stop (don't create)
}
ShutDownManager sm = InstanceManager.getDefault(jmri.ShutDownManager.class);
List<Callable<Boolean>> callables = sm.getCallables();
while (!callables.isEmpty()) {
Callable<Boolean> callable = callables.get(0);
fail("Test " + getTestClassName() + " left registered shutdown callable of type "
+ callable.getClass());
sm.deregister(callable);
callables = sm.getCallables(); // avoid ConcurrentModificationException
}
List<Runnable> runnables = sm.getRunnables();
while (!runnables.isEmpty()) {
Runnable runnable = runnables.get(0);
fail("Test " + getTestClassName() + " left registered shutdown runnable of type "
+ runnable.getClass());
sm.deregister(runnable);
runnables = sm.getRunnables(); // avoid ConcurrentModificationException
}
// use reflection to reset static fields in the class.
try {
Class<?> c = jmri.managers.DefaultShutDownManager.class;
Field f = c.getDeclaredField("shuttingDown");
f.setAccessible(true);
f.set(sm, false);
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException x) {
fail("Failed to reset DefaultShutDownManager shuttingDown field", x);
}
}
public static void initStartupActionsManager() {
InstanceManager.store(
new jmri.util.startup.StartupActionsManager(),
jmri.util.startup.StartupActionsManager.class);
}
public static void initConnectionConfigManager() {
InstanceManager.setDefault(ConnectionConfigManager.class, new ConnectionConfigManager());
}
public static void initRosterConfigManager() {
RosterConfigManager manager = new RosterConfigManager();
try {
manager.initialize(ProfileManager.getDefault().getActiveProfile());
} catch (InitializationException ex) {
log.error("Failed to initialize RosterConfigManager", ex);
}
InstanceManager.setDefault(RosterConfigManager.class, manager);
}
/**
* Use reflection to reset the jmri.Application instance.
*/
public static void resetApplication() {
try {
Class<?> c = jmri.Application.class;
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(new jmri.Application(), null);
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException x) {
log.error("Failed to reset jmri.Application static field", x);
}
}
/**
* Use reflection to reset the jmri.util.node.NodeIdentity instance.
*/
public static void resetNodeIdentity() {
try {
Class<?> c = jmri.util.node.NodeIdentity.class;
Field f = c.getDeclaredField("instance");
f.setAccessible(true);
f.set(c, null);
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException x) {
log.error("Failed to reset jmri.util.node.NodeIdentity instance", x);
}
}
public static void initGuiLafPreferencesManager() {
GuiLafPreferencesManager m = new GuiLafPreferencesManager();
InstanceManager.setDefault(GuiLafPreferencesManager.class, m);
}
/**
* Use only if profile contents are not to be verified or modified in test.
* If a profile will be written to and its contents verified as part of a
* test use {@link #resetProfileManager(jmri.profile.Profile)} with a
* provided profile.
* <p>
* The new profile will have the name {@literal TestProfile}, the id
* {@literal 00000000}, and will be in the directory {@literal temp}
* within the sources working copy.
*/
public static void resetProfileManager() {
try {
Profile profile = new NullProfile("TestProfile", "00000000", FileUtil.getFile(FileUtil.SETTINGS));
resetProfileManager(profile);
} catch (FileNotFoundException ex) {
log.error("Settings directory \"{}\" does not exist", FileUtil.SETTINGS);
} catch (IOException | IllegalArgumentException ex) {
log.error("Unable to create profile", ex);
}
}
/**
* Use when an isolated per-test profile directory is available (e.g. from
* {@literal @TempDir}). Guarantees a clean profile regardless of the host
* machine's JMRI settings directory.
*
* @param tempDir a writable directory for the profile, typically from {@literal @TempDir}
*/
public static void resetProfileManager(File tempDir) {
try {
resetProfileManager(new NullProfile(tempDir));
} catch (IOException ex) {
log.error("Unable to create profile in {}", tempDir, ex);
}
}
/**
* Use if the profile needs to be written to or cleared as part of the test.
* A temporary folder is suggested for the profile, see
* https://www.jmri.org/help/en/html/doc/Technical/JUnit.shtml#tempFileCreation
* <code>
* jmri.profile.Profile profile = new jmri.profile.NullProfile(temporaryFolder);
* JUnitUtil.resetProfileManager(profile);
* </code>
*
* @param profile the provided profile
*/
public static void resetProfileManager(Profile profile) {
ProfileManager.getDefault().setActiveProfile(profile);
InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).setEnableStoreCheck(false);
}
/**
* PreferencesProviders retain per-profile objects; reset them to force that
* information to be dumped.
*/
public static void resetPreferencesProviders() {
try {
// reset UI provider
Field providers = JmriUserInterfaceConfigurationProvider.class.getDeclaredField("PROVIDERS");
providers.setAccessible(true);
((Map<?, ?>) providers.get(null)).clear();
// reset XML storage provider
providers = JmriConfigurationProvider.class.getDeclaredField("PROVIDERS");
providers.setAccessible(true);
((Map<?, ?>) providers.get(null)).clear();
// reset java.util.prefs.Preferences storage provider
Field shared = JmriPreferencesProvider.class.getDeclaredField("SHARED_PROVIDERS");
Field privat = JmriPreferencesProvider.class.getDeclaredField("PRIVATE_PROVIDERS");
shared.setAccessible(true);
((Map<?, ?>) shared.get(null)).clear();
privat.setAccessible(true);
((Map<?, ?>) privat.get(null)).clear();
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
log.error("Unable to reset preferences providers", ex);
}
}
/**
* Silences the outputs from the Jemmy GUI Test framework.
*/
public static void silenceGUITestOutput() {
JUnitUtil.setGUITestOutput(TestOut.getNullOutput());
}
/**
* Sets the outputs for the Jemmy GUI Test framework to the defaults. Call
* this after setting up logging to enable outputs for a specific test.
*/
public static void verboseGUITestOutput() {
JUnitUtil.setGUITestOutput(new TestOut());
}
/**
* Set the outputs for the Jemmy GUI Test framework.
*
* @param output a container for the input, output, and error streams
*/
public static void setGUITestOutput(TestOut output) {
org.netbeans.jemmy.JemmyProperties.setCurrentOutput(output);
}
/**
* Service method to find the test class name in the traceback. Heuristic:
* First jmri or apps class that isn't this one.
* @return String class name
*/
static String getTestClassName() {
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
for (StackTraceElement e : trace) {
String name = e.getClassName();
if (name.startsWith("jmri") || name.startsWith("apps")) {
if (!name.endsWith("JUnitUtil")) {
return name;
}
}
}
return "<unknown class>";
}
/**
* Dispose of any disposable windows. This should only be used if there is
* no ability to actually close windows opened by a test using
* {@link #dispose(java.awt.Window)} or
* {@link #disposeFrame(java.lang.String, boolean, boolean)}, since this may
* mask other side effects that should be dealt with explicitly.
*
* @param warn log a warning for each window if true
* @param error log an error (failing the test) for each window if true
*/
public static void resetWindows(boolean warn, boolean error) {
resetWindows(warn, error, "in");
}
/**
* Dispose of any disposable windows. This should only be used if there is
* no ability to actually close windows opened by a test using
* {@link #dispose(java.awt.Window)} or
* {@link #disposeFrame(java.lang.String, boolean, boolean)}, since this may
* mask other side effects that should be dealt with explicitly.
*
* @param warn log a warning for each window if true
* @param error log an error (failing the test) for each window if true
* @param testLocation where in the JUnitUtil process the check is undertaken, e.g. before
*/
private static void resetWindows(boolean warn, boolean error, String testLocation ) {
// close any open remaining windows from earlier tests
Frame[] frames = Frame.getFrames();
for (Frame frame : frames) {
if (frame.isDisplayable()) {
if (frame.getClass().getName().equals("javax.swing.SwingUtilities$SharedOwnerFrame")) {
String message = "Cleaning up nameless invisible frame created by creating a dialog with a null parent {} {}.";
if (error) {
log.error(message, testLocation, getTestClassName());
} else if (warn) {
log.warn(message, testLocation, getTestClassName());
}
} else {
String message = "Cleaning up frame \"{}\" (a {}) {} {}.";
if (error) {
log.error(message, frame.getTitle(), frame.getClass(), testLocation, getTestClassName());
} else if (warn) {
log.warn(message, frame.getTitle(), frame.getClass(), testLocation, getTestClassName());
}
}
JUnitUtil.dispose(frame);
}
}
Window[] windows = Window.getWindows();
for (Window window : windows) {
if (window.isDisplayable()) {
if (window.getClass().getName().equals("javax.swing.SwingUtilities$SharedOwnerFrame")) {
String message = "Cleaning up nameless invisible window created by creating a dialog with a null parent {} {}.";
if (error) {
log.error(message, testLocation, getTestClassName());
} else if (warn) {
log.warn(message, testLocation, getTestClassName());
}
} else {
String message = "Cleaning up window \"{}\" (a {}) {} {}.";
if (error) {
log.error(message, window.getName(), window.getClass(), testLocation, getTestClassName());
} else if (warn) {
log.warn(message, window.getName(), window.getClass(), testLocation, getTestClassName());
}
}
JUnitUtil.dispose(window);
}
}
}
/**
* Dispose of a visible frame searched for by title.
* Disposes of the first visible frame found with the given title.
* Asserts that the calling test failed if the frame cannot be found.
*
* @param title the title of the frame to dispose of.
* @param subString true to match title param as a substring of the frame's
* title; false to require an exact match
* @param caseSensitive true if search is case sensitive; false otherwise
*/
public static void disposeFrame(String title, boolean subString, boolean caseSensitive) {
Frame frame = FrameWaiter.getFrame(title, subString, caseSensitive);
if (frame != null) {
JUnitUtil.dispose(frame);
} else {
Assertions.fail("Unable to find frame \"" + title + "\" to dispose.");
}
}
/**
* Dispose of a window. Disposes of the window on the GUI thread, returning
* only after the window is disposed of.
*
* @param window the window to dispose of
*/
public static void dispose(@Nonnull Window window) {
java.util.Objects.requireNonNull(window, "Window cannot be null");
ThreadingUtil.runOnGUI(() -> {
window.setVisible(false);
window.dispose();
});
}
/**
* Wait for a thread to terminate, ie is no longer alive.
* A non-existent Thread is not an test failure.
* A Thread which does not complete in time IS a test failure.
* @param threadName full name of the Thread to wait for.
*/
public static void waitThreadTerminated( String threadName ) {
Thread t = getThreadByName( threadName );
if ( t != null ) {
waitFor( () -> !t.isAlive(), "Thread \"" + threadName + "\" is still alive");
}
}
/**
* Wait for a thread to terminate, ie is no longer alive.
* A Thread which does not complete in time is a test failure.
* @param thread the Thread to wait for.
*/
public static void waitThreadTerminated( @Nonnull Thread thread ) {
waitFor( () -> !thread.isAlive(), "Thread \"" + thread.getName() + "\" is still alive");
}
/**
* Get a Thread by matching the name.
* @param threadName Starting characters of the Thread name.
* @return the Thread, null if no Thread found.
*/
@CheckForNull
public static Thread getThreadByName(String threadName) {
for (Thread t : Thread.getAllStackTraces().keySet()) {
if (t.getName().equals(threadName)) {
return t;
}
}
return null;
}
/**
* Get a Thread with a name starting with the supplied String.
* @param threadName Name of the Thread.
* @return the Thread, null if no Thread found.
*/
@CheckForNull
public static Thread getThreadStartsWithName(String threadName) {
for (Thread t : Thread.getAllStackTraces().keySet()) {
if (t.getName().startsWith(threadName)) {
return t;
}
}
return null;
}
static SortedSet<String> threadNames = new TreeSet<>(Arrays.asList(new String[]{
// names we know about from normal running
"main",
"Java2D Disposer",
"AWT-Shutdown",
"AWT-EventQueue",
"AWT-XAWT", // seen on Jenkins Ubuntu
"GC Daemon",
"Finalizer",
"Reference Handler",
"Signal Dispatcher", // POSIX signals in JRE, not trains signals
"Java2D Queue Flusher",
"Time-limited test",
"WindowMonitor-DispatchThread",
"RMI Reaper",
"RMI TCP Accept",
"RMI GC Daemon",
"TimerQueue",
"Java Sound Event Dispatcher",
"Aqua L&F", // macOS
"AppKit Thread",
"JMRI Common Timer",
"BluecoveAsynchronousShutdownThread", // from LocoNet BlueTooth implementation
"Keep-Alive-Timer", // from "system" group
"process reaper", // observed in macOS JRE
"SIGINT handler", // observed in JmDNS; clean shutdown takes time
"Multihomed mDNS.Timer", // observed in JmDNS; clean shutdown takes time
"Direct Clip", // observed in macOS JRE, associated with javax.sound.sampled.AudioSystem
"Basic L&F File Loading Thread",
"dns.close in ZeroConfServiceManager#stopAll",
"Common-Cleaner",
"Batik CleanerThread" // XML
}));
static List<Thread> threadsSeen = new ArrayList<>();
/**
* Do a diagnostic check of threads,
* providing a traceback if any new ones are still around.
* <p>
* First implementation is rather simplistic.
*/
static void handleThreads() {
// now check for extra threads
ThreadGroup main = Thread.currentThread().getThreadGroup();
while (main.getParent() != null ) {main = main.getParent(); }
Thread[] list = new Thread[main.activeCount()+2]; // space on end
int max = main.enumerate(list);
for (int i = 0; i<max; i++) {
Thread t = list[i];
if (t.getState() == Thread.State.TERMINATED) { // going away, just not cleaned up yet
threadsSeen.remove(t); // don't want to prevent gc
continue;
}
if (threadsSeen.contains(t)) continue;
String name = t.getName();
ThreadGroup g = t.getThreadGroup();
String group = (g != null) ? g.getName() : "<null group>";
if (! (
threadNames.contains(name)
|| group.equals("system")
|| name.startsWith("Timer-") // we separately scan for JMRI-resident timers
|| name.startsWith("RMI TCP Accept")
|| name.startsWith("AWT-EventQueue")
|| name.startsWith("Aqua L&F")
|| name.startsWith("junit-jupiter-") // JUnit
|| name.startsWith("Image Fetcher ")
|| name.startsWith("Image Animator ")
|| name.startsWith("JmDNS(")
|| name.startsWith("JmmDNS pool")
|| name.startsWith("JNA Cleaner")
|| name.startsWith("ForkJoinPool.commonPool-worker")
|| name.startsWith("SocketListener(")
|| name.startsWith("Libgraal")
|| name.startsWith("LibGraal")
|| name.startsWith("TruffleCompilerThread-")
|| name.startsWith("surefire-forkedjvm-")
|| ( name.startsWith("pool-") && name.endsWith("thread-1") )
|| group.contains("FailOnTimeoutGroup") // JUnit timeouts
|| ( name.equals("Cleaner-0") && group.contains("InnocuousThreadGroup") ) // Created indirectly by ScriptEngineSelector
// Threads created by OpenLCB which JMRI cannot end
|| ( name.equals("openlcb-hub-output") && group.contains("main") )
|| ( name.equals("OpenLCB Mimic Node Store Timer") && group.contains("main") )
|| ( name.equals("OpenLCB-datagram-timer") && group.contains("main") )
|| ( name.startsWith("Olcb-Pool-") && group.contains("main") )
|| ( name.equals("OpenLCB Memory Configuration Service Retry Timer") && group.contains("main") )
|| ( name.equals("OpenLCB NIDaAlgorithm Timer") && group.contains("main") )
|| ( name.equals("OpenLCB LoaderClient Timeout Timer") && group.contains("main") )
|| ( name.equals("OLCB Interface dispose thread") && group.contains("main") )
|| ( name.equals("olcbCanInterface.initialize") && group.contains("JMRI") ) // Created by JMRI but hangs due to OpenLCB lib
|| ( name.startsWith("SwingWorker-pool-") &&
( group.contains("FailOnTimeoutGroup") || group.contains("main") )
)
)) {
if (t.getState() == Thread.State.TERMINATED) {
// might have transitioned during above (logging slow)
continue;
}
// This thread we have to deal with.
boolean kill = true;
String action = "Interrupt";
if (!killRemnantThreads) {
action = "Found";
kill = false;
}
// for anonymous threads, show the traceback in hopes of finding what it is
if (name.startsWith("Thread-")) {
StackTraceElement[] traces = Thread.getAllStackTraces().get(t);
if (traces == null) continue; // thread went away, maybe terminated in parallel
if (traces.length >7 && traces[7].getClassName().contains("org.netbeans.jemmy") ) {
// empirically. jemmy leaves anonymous threads
String details = traces[7].getClassName() + "." + traces[7].getMethodName()
+" [" + traces[7].getFileName() + "." + traces[7].getLineNumber() + "]";
log.warn("Jemmy remnant thread running {}", details );
if ( failRemnantThreads ) {
threadsSeen.add(t);
Assertions.fail("Jemmy remnant thread running " + details);
}
} else {
// anonymous thread that should be displayed
Exception ex = new Exception("traceback of numbered thread");
ex.setStackTrace(traces);
log.warn("{} remnant thread \"{}\" in group \"{}\" after {}", action, name, group, getTestClassName(), ex);
if ( failRemnantThreads ) {
threadsSeen.add(t);
Assertions.fail("Thread \"" + name + "\" after " + getTestClassName());
}
}
} else {
log.warn("{} remnant thread \"{}\" in group \"{}\" after {}", action, name, group, getTestClassName());
if ( failRemnantThreads ) {
threadsSeen.add(t);
Assertions.fail("Thread \"" + name + "\" in group \"" + group + "\" after " + getTestClassName());
}
}
if (kill) {
killThread(t);
} else {
threadsSeen.add(t);
}
}
}
}
/* Global Panel operations */
/**
* Close all panels associated with the {@link EditorManager} default
* instance.
*/
public static void closeAllPanels() {
InstanceManager.getOptionalDefault(EditorManager.class)
.ifPresent(m -> m.getAll()
.forEach(e -> {
if(e.isVisible()){
e.requestFocus();
try {
EditorFrameOperator editorFrameOperator = new EditorFrameOperator(e.getTargetFrame());
editorFrameOperator.closeFrameWithConfirmations();
} catch (TimeoutExpiredException timeoutException ) {
log.error("Failed to close panel {} with exception {}",e.getTitle(),
timeoutException.getMessage(),
LoggingUtil.shortenStacktrace(timeoutException));
}
}
e.dispose();
}));
EditorFrameOperator.clearEditorFrameOperatorThreads();
}
/* GraphicsEnvironment utility methods */
/**
* Get the content pane of a dialog.
*
* @param title the dialog title
* @return the content pane
*/
public static Container findContainer(String title) {
return new JDialogOperator(title).getContentPane();
}
/**
* Press a button after finding it in a container by title.
*
* @param frame container containing button to press
* @param text button title
* @return the pressed button
*/
public static AbstractButton pressButton(Container frame, String text) {
AbstractButton button = JButtonOperator.findAbstractButton(frame, text, true, true);
Assertions.assertNotNull( button, () -> text + " Button not found");
AbstractButtonOperator abo = new AbstractButtonOperator(button);
abo.doClick();
return button;
}
private static final Random random = new Random();
public static Random getRandom(){
return random;
}
private static final Random randomConstantSeed = new Random(0);
public static Random getRandomConstantSeed(){
return randomConstantSeed;
}
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JUnitUtil.class);
}