package junit.extensions;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import com.windowtester.event.swt.UIProxy;
import com.windowtester.finder.swt.ShellFinder;
import com.windowtester.internal.debug.IRuntimePluginTraceOptions;
import com.windowtester.internal.debug.TraceHandler;
import com.windowtester.internal.runtime.Platform;
import com.windowtester.runtime.monitor.IUIThreadMonitor;
import com.windowtester.runtime.monitor.IUIThreadMonitorListener;
import com.windowtester.runtime.swt.internal.debug.LogHandler;
import com.windowtester.runtime.swt.internal.display.DisplayIntrospection;
import com.windowtester.runtime.swt.internal.preferences.PlaybackSettings;
import com.windowtester.runtime.swt.internal.state.MenuWatcher;
import com.windowtester.runtime.util.ScreenCapture;
import com.windowtester.runtime.util.TestMonitor;
import com.windowtester.swt.IUIContext;
import com.windowtester.swt.RuntimePlugin;
import com.windowtester.swt.UIContextFactory;
import com.windowtester.swt.WaitTimedOutException;
import com.windowtester.swt.WidgetSearchException;
import com.windowtester.swt.runtime.settings.TestSettings;
import com.windowtester.swt.util.ExceptionHandlingHelper;
/*******************************************************************************
* Copyright (c) 2012 Google, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Google, Inc. - initial API and implementation
*******************************************************************************/
public class UITestCase extends TestCase
implements IUIThreadMonitorListener
{
/**
* A constant used to specify how many levels of menu to dismiss in
* {@link #dismissUnexpectedMenus()}.
*/
private static final int MAX_MENU_DEPTH = 5;
/**
* A flag to indicate whether a test should FAIL when menus/shells
* are dismissed/closed at the end of a test.
*/
private final boolean UNEXPECTED_SHELLS_MENUS_TREATED_AS_FAILURES = false;
/**
* The application class to be launched by calling the static main method with the
* specified arguments (see {@link #_launchArgs}) in a separate thread, or
* <code>null</code> if no application is to be launched.
*/
private final Class _launchClass;
/**
* The arguments to be passed to the static main method of the application class to be
* launched (see {@link #_launchClass}, or <code>null</code> if no application is
* to be launched.
*/
private String[] _launchArgs;
/**
* The application display associated with this test or <code>null</code> if it has
* not been cached yet.
*
* @see #launchApp()
* @see #cacheDisplay()
*/
private Display _display;
/**
* The UI Context instance associated with the receiver.
*/
private IUIContext _uiContext;
/**
* The root shell associated with this test or <code>null</code> if none. At the end
* of the test, any decendent shells of this shell will be forcefully closed.
*/
private Shell _rootShell;
/**
* Flag indicating whether or not the test is currently executing.
*/
private boolean _testRunning;
/**
* Collection of seen classes for managing oneTimeSetup
*/
private static Set _seenClasses = new HashSet();
/**
* Counter used for managing oneTimeTeardown
*/
private static int _testsToRun = 0;
// Cached exceptions for re-throwing
private InvocationTargetException _ite;
private IllegalAccessException _iae;
// ///////////////////////////////////////////////////////////////////////////////
//
// Constructors
//
// ///////////////////////////////////////////////////////////////////////////////
/**
* Create an instance.
*/
public UITestCase() {
this((Class)null);
}
/**
* Create an instance with the given name.
*/
public UITestCase(String testName) {
super(testName);
_launchClass = null;
initTest();
}
/**
* Create an instance that will launch and test the specified application class.
*
* @param launchClass - The application class to be launched by calling the static
* main method with the specified arguments (see ) in a separate thread, or
* <code>null</code> if no application is to be launched.
*/
public UITestCase(Class launchClass) {
this(launchClass, null);
}
/**
* Create an instance that will launch and test the specified application class.
*
* @param launchClass - The application class to be launched by calling the static
* main method with the specified arguments (see ) in a separate thread, or
* <code>null</code> if no application is to be launched.
* @param launchArgs - The arguments to be passed to the static main method of the
* application class to be launched, or <code>null</code> if no
* application is to be launched.
*/
public UITestCase(Class launchClass, String[] launchArgs) {
_launchClass = launchClass;
_launchArgs = launchArgs;
initTest();
}
// //////////////////////////////////////////////////////////////////////////
//
// Initialization and access
//
// //////////////////////////////////////////////////////////////////////////
/**
* Increment our test count -- for onetime teardown management.
*/
private void initTest() {
++_testsToRun;
}
/**
* The application class specified in the constructor is launched in a separate thread
* by calling the static main method with the arguments specified in the constructor.
* The current thread then waits for an active Shell to become available and
* initializes the display associated with the receiver to be that Shell's display.
*/
protected void launchApp() {
if (_launchClass != null)
setDisplay(launchApp(_launchClass, _launchArgs));
}
/**
* The specified application class is launched in a separate thread by calling the
* static main method with the specified arguments. The current thread then waits for
* an active Shell to become available and answers that Shell's display.
*
* @param mainApp - The application class to be launched (not <code>null</code>)
* @param args - Arguments for the main method
* @return the {@link Display} for the launched application (not <code>null</code>)
*/
public static Display launchApp(final Class mainApp, final String[] args) {
// TODO [author=Dan] Move this method to some new bootstrap utility class
if (mainApp == null)
throw new IllegalArgumentException("main application class cannot be null");
Thread t = new Thread(new Runnable() {
public void run() {
try {
Method main = mainApp.getMethod("main", new Class[]{
String[].class
});
Object[] realArgs = args != null ? args : new String[]{null};
main.invoke(main, realArgs);
}
catch (Exception e) {
LogHandler.log(e);
}
}
});
t.start();
return new DisplayIntrospection(10000).syncIntrospect();
}
/**
* Answers the display.
*
* @return the display (not <code>null</code>)
* @throws IllegalStateException if the display has not been initialized
*/
public Display getDisplay() {
if (_display == null)
throw new IllegalStateException("Display has not been initialized.");
return _display;
}
/**
* Cache the current {@link Display} if it has not already been cached by the
* {@link #launchApp()} method or a prior call to this method.
*/
protected void cacheDisplay() {
if (_display == null)
// TODO[pq]: is this right?
// can we safely do this in the RCP/workbench test case?
setDisplay(Display.getCurrent());
}
/**
* Sets the display.
*
* @param display The display (not <code>null</code>)
* @throws {@link IllegalStateException} if the display has already been initialized
* via the {@link #setDisplay(Display)} method.
*/
protected void setDisplay(Display display) {
if (display == null)
throw new IllegalArgumentException("Display cannot be null");
if (_display != null)
throw new IllegalStateException("Display has already been initialized.");
_display = display;
}
/**
* Get the {@link IUIContext} associated with this test.
*
* @return the user interface context instance (not <code>null</code>)
* @throws IllegalStateException if the context has not been initialized
*/
protected final IUIContext getUIContext() {
if (_uiContext == null)
throw new IllegalArgumentException("UI context has not been initialized");
return _uiContext;
}
/**
* Cache the {@link IUIContext} associated with this test if it has not already been
* cached by prior call to this method.
*/
protected void cacheUIContext() {
if (_uiContext == null){
// PlaybackSettings settings = getPlaybackSettings();
// if(settings.getExperimentalPlaybackOn()){
// setUIContext(UIContextFactory.createContext(SwtPlaybackContext.class, getDisplay()));
// }else{
setUIContext(UIContextFactory.createContext(getDisplay()));
// }
}
}
protected PlaybackSettings getPlaybackSettings() {
return Platform.isRunning() ? RuntimePlugin.getDefault().getPlaybackSettings() : PlaybackSettings.loadFromFile();
}
/**
* Set the user interface context associated with this test
*
* @param context the user interface context (not <code>null</code>)
* @throws IllegalStateException if the context has already been initialized.
*/
protected void setUIContext(IUIContext context) {
if (context == null)
throw new IllegalArgumentException("UI context cannot be null");
if (_uiContext != null)
throw new IllegalStateException("UI context has already been initialized");
_uiContext = context;
}
/**
* Answer the root shell associated with this test or <code>null</code> if none has
* been specified.
*
* @return the root shell or <code>null</code>
*/
protected Shell getRootShell() {
return _rootShell;
}
/**
* Cache the active shell so that any decendent shells can be forcefully closed at the
* end of the test.
*/
protected void cacheRootShell() {
getDisplay().syncExec(new Runnable() {
public void run() {
setRootShell(getDisplay().getActiveShell());
}
});
}
/**
* Set the root shell associated with the receiver. At the end of the test, any
* decendent shells of this shell that are still open will be forcefully closed.
*
* @param shell the shell or <code>null</code>
*/
protected void setRootShell(Shell shell) {
_rootShell = shell;
}
/**
* A hook to register widgets with the ui context.
*/
protected void registerWidgetInfo() {
// overriden in subclasses
}
// //////////////////////////////////////////////////////////////////////////
//
// Cleanup
//
// //////////////////////////////////////////////////////////////////////////
/**
* Called after tearDown is called to perform common cleanup such as calling
* {@link #closeUnexpectedShells()} and {@link #dismissUnexpectedMenus()}.
*/
protected void cleanUp() {
dismissUnexpectedMenus();
closeUnexpectedShells();
}
/**
* Called at the end of the test to forcefully close shells that should not be open.
* and guarantee that no (blocking) dialogs are still open. By default, this method
* closes any shells decendent from the root shell but not the root shell itself. This
* is necessary so that the junit thread can proceed. Subclasses may override or
* extend to close a different set of shells.
*
* @see #setRootShell(Shell)
*/
protected void closeUnexpectedShells() {
closeDecendentShells(getRootShell());
}
/**
* Called at the end of the test to dismiss menus that were left open at the end of
* the test.
*
*/
protected void dismissUnexpectedMenus() {
//if a menu is open, take a screenshot and dismiss
if (MenuWatcher.getInstance(_display).isMenuOpen()) {
if (UNEXPECTED_SHELLS_MENUS_TREATED_AS_FAILURES) {
createScreenCapture("unexpected menu");
//register an exception if there isn't one already
if (_ite == null)
_ite = new InvocationTargetException(new AssertionFailedError("Menu left open at end of test"));
}
//TODO: this may be OS-specific...
for (int i= 0; i <= MAX_MENU_DEPTH; ++i) {
getUIContext().keyClick(SWT.ESC); //close menu by hitting ESCAPE
}
}
}
/**
* Forcefully close any shells decendent from the specified shell but does not close
* the specified shell itself.
*
* @param shell the shell
*/
protected void closeDecendentShells(final Shell shell) {
// guard against case where display is already disposed
Display display = getDisplay();
if (!display.isDisposed() && shell != null && !shell.isDisposed())
display.syncExec(new Runnable() {
public void run() {
if (!shell.isDisposed()) {
// Close all blocking dialogs. Necessary, otherwise the junit
// thread does not proceed.
Shell[] shells = shell.getShells();
closeShells(shells);
}
}
private void closeShells(final Shell[] shells) {
for (int i = 0; i < shells.length; i++) {
if (!shells[i].isDisposed()) {
closeShells(shells[i].getShells());
}
if (!shells[i].isDisposed()) {
if (UNEXPECTED_SHELLS_MENUS_TREATED_AS_FAILURES) {
// we specifically care about modal shells...
if (ShellFinder.isModal(shells[i])) {
createScreenCapture("forcing shell close: "
+ shells[i].getText());
/*
* If there is no error associated, signal
* one
*/
if (_ite == null)
_ite = new InvocationTargetException(
new AssertionFailedError(
"Shell left open at end of test"));
}
}
closeShell(shells[i]);
}
}
}
private void closeShell(Shell shell) {
/*
* Forcefully closing toolitp shells causes the workbench to get totally wedged.
* It's VERY difficult to reliably identify tooltip shells; as a stop-gap we are
* ignoring ALL non-modal shells in cleanup. If this is unsafe, we'll have to
* figure out what non-modal dialogs are a problem and close only them.
*
*/
String shellDescription = UIProxy.getToString(shell);
if (ShellFinder.isModal(shell)) {
shell.close();
// shells[i].dispose();
TraceHandler.trace(IRuntimePluginTraceOptions.BASIC, "Test cleanup closing shell: " + shellDescription);
} else {
TraceHandler.trace(IRuntimePluginTraceOptions.BASIC, "Test cleanup skipping shell: " + shellDescription + " [not modal]");
}
}
});
}
// ///////////////////////////////////////////////////////////////////////////////
//
// Test Execution
//
// ///////////////////////////////////////////////////////////////////////////////
/**
* Runs the bare test sequence.
*
* @see junit.framework.TestCase#runBare()
*/
public final void runBare() throws Throwable {
TestSettings.getInstance().push();
if (_testRunning)
throw new IllegalStateException("test is already running");
_testRunning = true;
TestMonitor.getInstance().beginTestCase(this);
--_testsToRun;
launchApp();
cacheDisplay();
cacheUIContext();
cacheRootShell();
registerWidgetInfo();
// TODO [author=Dan] merge the two test monitors
startTestMonitor();
try {
launchTestThread();
waitUntilFinished();
}
catch (Throwable e) {
handleException(e);
}
stopTestMonitor();
TestMonitor.getInstance().endTestCase();
TestSettings.getInstance().pop();
}
/**
* Launch a seperate test thread that will call {@link TestCase#setUp()}, test and
* {@link TestCase#tearDown()} and interact with the UI thread through the
* {@link IUIContext} associated with this test.
*/
protected void launchTestThread() {
Thread testThread = new Thread(getName()) {
public void run() {
try {
// System.out.println("running test in " + Thread.currentThread());
try {
try {
UITestCase.this.doOneTimeSetup();
}catch (Throwable e) {
handleException(e);
}
try {
UITestCase.this.setUp();
}
catch (Throwable e) {
handleException(e);
}
try {
UITestCase.this.runTest();
/*
* post test-run, check for any active conditions to properly
* cleanup (close open dialogs, etc)
*/
// TODO [author=Dan] make this IUIContext API ?
((IUIContext) getUIContext()).handleConditions();
}
catch (Throwable e) {
handleException(e);
}
finally {
try {
tearDown();
}
catch (Throwable e) {
handleException(e);
}
try {
UITestCase.this.doOneTimeTearDown();
}catch (Throwable e) {
handleException(e);
}
}
}
finally {
try {
cleanUp();
}
catch (Throwable e) {
// internal error; no screen capture
logException(e);
}
}
}
catch (InvocationTargetException e) {
// e.printStackTrace();
e.fillInStackTrace();
_ite = e;
}
catch (IllegalAccessException e) {
// e.printStackTrace();
e.fillInStackTrace();
_iae = e;
}
catch (Throwable e) {
// e.printStackTrace();
// e.fillInStackTrace();
_ite = new InvocationTargetException(e);
}
finally {
_testRunning = false;
// guard against case where display is already disposed
if (!getDisplay().isDisposed())
getDisplay().wake();
}
}
};
testThread.setDaemon(true);
testThread.start();
}
/**
* Perform one time setup (internal).
*/
private void doOneTimeSetup() throws Exception {
//only exec onetime setup if class has not been seen
if (_seenClasses.add(getClass())) {
TestSettings.getInstance().push(); //create fresh settings scope
oneTimeSetup();
}
}
/**
* Performs one time setup of the test fixture. Called once per test class, before
* setup.
*/
protected void oneTimeSetup() throws Exception {
//default: no-op
}
/**
* Perform one time setup (internal).
*/
private void doOneTimeTearDown() throws Exception {
//only exec onetime teardown if all the tests have been run
//System.out.println("tests to run: " + _testsToRun);
if (_testsToRun == 0) {
oneTimeTearDown();
TestSettings.getInstance().pop();
}
}
/**
* Performs one time teardown of the test fixture. Called once per test class, after
* teardown.
*/
protected void oneTimeTearDown() throws Exception {
//default: no-op
}
/**
* Read and dispatch UI events until the test thread is finished. Exceptions thrown by
* tests are re-thrown.
*
* @throws Throwable - a test-thrown assertion/exception
*/
private void waitUntilFinished() throws Throwable {
Display display = getDisplay();
// Loop until test completes or there is an exception
while (_testRunning && _ite == null && _iae == null && !display.isDisposed()) {
try {
if (!display.isDisposed() && !display.readAndDispatch()) {
display.sleep();
}
}
catch (SWTException ex) {
// Do nothing: rethrowing errors blocks the display thread.
// One must rely on the fact of proper error handling of
// display thread users.
//!pq: this was the abbot assumption, but is it right? why not cache and fail later?
LogHandler.log("Exception caught in UITestCase.waitUntilFinished():");
LogHandler.log(ex);
}
catch (Throwable t) {
LogHandler.log("Exception caught in UITestCase.waitUntilFinished():");
LogHandler.log(t);
}
}
// Throw all exceptions caught inside the test thread
if (_ite != null) {
// Extract the wrappered exception as appropriate
if (_ite.getCause() instanceof AssertionFailedError)
throw _ite.getCause();
throw _ite;
}
if (_iae != null)
throw _iae;
}
/**
* Start monitoring the UI for responsiveness.
*/
protected void startTestMonitor() {
getUIThreadMonitor().setListener(this);
}
/**
* Stop monitoring the UI for responsiveness.
*/
protected void stopTestMonitor() {
getUIThreadMonitor().setListener(null);
}
/**
* Answer the user interface thread monitor used to determine if the user interface
* thread is idle or unresponsive longer than some expected time.
*
* @return the user interface thread monitor (not <code>null</code>)
*/
protected IUIThreadMonitor getUIThreadMonitor() {
return (IUIThreadMonitor) getUIContext().getAdapter(IUIThreadMonitor.class);
}
/**
* Process the exception by logging it, taking a screen capture, and rethrowing the
* exception.
*
* @param e the Exception
* @throws Throwable rethrows the passed exception
*/
protected void handleException(Throwable e) throws Throwable {
logException(e);
/*
* To avoid creating multiple screenshots for the same exception (which might
* get handled multiple times as it pops us the call stack) we check to see if
* the exception has already been captured.
*/
//!pq: re-enabled to address:
if (_iae == null &&_ite == null) {
/*
* WidgetSearch and Wait Exceptions are handled in UIContext, so all
* we have to handle here are others.
*
* NOTE: if a user throws one of these exceptions themselves, the screenshots won't happen as they expect...
*/
if (!(e instanceof WidgetSearchException) && !(e instanceof WaitTimedOutException)) {
createScreenCapture(e.getMessage());
}
}
throw e;
}
private void createScreenCapture(String desc) {
TraceHandler.trace(IRuntimePluginTraceOptions.CONDITIONS, "Creating screenshot ("+ desc +") for testcase: " + getId());
ScreenCapture.createScreenCapture(getId());
}
/**
* Log the exception.
*
* @param e the Exception
*/
protected void logException(Throwable e) {
LogHandler.log(e);
}
/**
* Called by the user interface thread monitor if the UI thread is idle for longer
* than some expected time.
*
* @see com.windowtester.swt.monitor.IUIThreadMonitorListener#uiTimeout(boolean)
*/
public void uiTimeout(boolean isIdle) {
_ite = new InvocationTargetException(new WaitTimedOutException("UI thread idle timeout"));
// do a screenshot before shutting down
ScreenCapture.createScreenCapture(getId());
// TODO [author=Dan] How can shells be closed or the test be stopped if UI thread is busy?
// TODO [author=Dan] Can close shells be done by the closeUnexpectedShells method?
if (isIdle)
new ExceptionHandlingHelper(getDisplay(), true).closeOpenShells();
}
/**
* The test identifier for screen captures
*
* @return the test id (not <code>null</code>)
*/
private String getId() {
return TestMonitor.getInstance().getCurrentTestCaseID();
}
}