/*******************************************************************************
* 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
*******************************************************************************/
package com.windowtester.runtime.swt.internal;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import abbot.script.Condition;
import abbot.tester.swt.ShellTester;
import com.windowtester.internal.debug.IRuntimePluginTraceOptions;
import com.windowtester.internal.debug.TraceHandler;
import com.windowtester.runtime.IUIContext;
import com.windowtester.runtime.swt.internal.debug.LogHandler;
import com.windowtester.runtime.swt.internal.selector.UIDriver;
import com.windowtester.runtime.util.ScreenCapture;
import com.windowtester.runtime.util.TestMonitor;
/**
* Used to close open shells before throwing exceptions (required, else the
* UI thread blocks).
*
*/
public class ExceptionHandlingHelper {
private static final int CALL_TO_NUCLEAR_CLOSE_INTERVAL = 1000;
private static final int CLOSE_ALL_WAIT_INTERVAL = 200;
private static final int CLOSE_ALL_WAIT_THRESHOLD = 10000;
private final Display _display;
private final boolean _captureScreens;
private final ShellTester _shellTester = new ShellTester();
//used to keep track of our last close call to guard against duplicates (needed since we're in an async exec)
private Shell _lastDisposed;
//used to track how many shells have been closed vs. requests so we know when to exit
private int _disposedCount;
private int _closeCalls;
private IUIContext ui;
class ShellCloser {
private final boolean _screenCaptureOnFirst;
/*
* flag to track first close
* (first does not generate a capture since it will have already been done)
* ---> this can be overrriden by setting <code>_closeOnFirst</code>
*/
private boolean _first = true;
public ShellCloser(boolean screenCaptureOnFirst) {
_screenCaptureOnFirst = screenCaptureOnFirst;
}
public void closeShells() {
handleConditions();
closeShells(getShells());
}
private void handleConditions() {
if (ui == null)
return;
ui.handleConditions();
}
private void closeShells(final Shell[] shells) {
//play it extra safe:
if (shells == null)
return;
Shell root = null;
for (int i = 0; i < shells.length; i++) {
root = shells[i];
//if not disposed, close children
if (root != null && !root.isDisposed()) {
try {
closeShells(getShells(root));
// if the root is modal, close it
if (isModal(root)) {
if (!isLast(root)) // but only if we haven't already requested it!
doClose(root);
}
} catch (SWTException e) {
e.printStackTrace();
/*
* Despite the fact that we test for disposal above, it
* may happen in the process of closing As a guard, we
* just consume the exception and continue forth...
*/
}
}
}
}
//is this the last shell we asked to close?
private boolean isLast(Shell shell) {
return shell == _lastDisposed;
}
private void doClose(Shell root) {
_lastDisposed = root;
String shellTitle = getText(root);
LogHandler.log("closing shell [aysnc] " + shellTitle);
if (captureScreens() && (!_first || _screenCaptureOnFirst))
doScreenCapture("pre close of modal shell: " + shellTitle);
closeShell(root);
_first = false; //not first anymore
}
}
public ExceptionHandlingHelper(Display display, boolean captureScreens) {
_display = display;
_captureScreens = captureScreens;
}
//pass an optional UI context (used as a callback to handle conditions during shell closing)
public ExceptionHandlingHelper(Display display, boolean captureScreens, IUIContext ui) {
this(display, captureScreens);
this.ui = ui;
}
/**
* Close all open modal shells, closing children first.
*/
public void closeOpenShells() {
//screenshot will have been taken at cause of failure
closeModalShellsNuclearOption(false);
//we call this 5 times to handle nested shells
for (int i=0; i < 5; ++i) {
//NOTE: there is a bit of a race here... we might consider a test for modal shells
//but that would involve a race too. Since this is a corner case, we just cross our
//fingers that the number of retries and our waits will suffice
UIDriver.pause(CALL_TO_NUCLEAR_CLOSE_INTERVAL);
closeModalShellsNuclearOption(true); //subsequent closes should trigger a capture
}
//TODO: create a wait util that uses IConditions
UIDriver.wait(new Condition() {
public boolean test() {
return _disposedCount == _closeCalls;
}
}, CLOSE_ALL_WAIT_THRESHOLD, CLOSE_ALL_WAIT_INTERVAL);
}
/**
* Forcefully close all modal shells.
* <p>
* Used as a last ditch effort to close open shells that won't seem to go away.
* <p>
* Taken from {@link junit.extensions.UITestCase} and modified.
*/
private void closeModalShellsNuclearOption(final boolean closeOnFirst) {
// guard against case where display is already disposed
if (getDisplay().isDisposed())
return; //shouldn't happen but let's be extra safe
new ShellCloser(closeOnFirst).closeShells();
}
//////////////////////////////////////////////////////////////////////////////
//
// Accessors
//
//////////////////////////////////////////////////////////////////////////////
private boolean captureScreens() {
return _captureScreens;
}
private Display getDisplay() {
return _display;
}
private ShellTester getShellTester() {
return _shellTester;
}
//////////////////////////////////////////////////////////////////////////////
//
// Predicates
//
//////////////////////////////////////////////////////////////////////////////
/**
* Test this shell to see if it's modal.
* @return <code>true</code> if the shell is modal, <code>false</code> otherwise
*/
private boolean isModal(Shell shell) {
int mask = SWT.PRIMARY_MODAL | SWT.APPLICATION_MODAL | SWT.SYSTEM_MODAL;
return ((getStyle(shell) & mask) != 0);
}
//////////////////////////////////////////////////////////////////////////////
//
// UI Thread access proxies
//
//////////////////////////////////////////////////////////////////////////////
private void closeShell(final Shell shell) {
++_closeCalls;
//notice this is an ASYNC -- this is because some shell closes can cause others to
//open which blocks the syncExec
getDisplay().asyncExec(new Runnable() {
public void run() {
shell.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
++_disposedCount;
}
});
shell.close();
}
});
}
private int getStyle(final Shell shell) {
final int[] style = new int[1];
getDisplay().syncExec(new Runnable() {
public void run() {
style[0] = shell.getStyle();
}
});
return style[0];
}
private Shell[] getShells() {
final Shell[][] shells = new Shell[1][];
getDisplay().syncExec(new Runnable() {
public void run() {
shells[0] = getDisplay().getShells();
}
});
return shells[0];
}
private Shell[] getShells(final Shell root) {
final Shell[][] shells = new Shell[1][];
getDisplay().syncExec(new Runnable() {
public void run() {
try {
if (!root.isDisposed())
shells[0] = root.getShells();
} catch(SWTException e) {
/*
* Although we test for disposal BEFORE calling this,
* there is a race, as the shell may get disposed DURING
* this exec... The solution is to just ignore the exception
* and return a null Shell that will get ignored by the call to
* close.
*
*
*/
}
}
});
return shells[0];
}
private String getText(final Shell shell) {
final String[] text = new String[1];
getDisplay().syncExec(new Runnable() {
public void run() {
text[0] = shell.getText();
}
});
return text[0];
}
//////////////////////////////////////////////////////////////////////////////
//
// Debugging Helpers
//
//////////////////////////////////////////////////////////////////////////////
/**
* Take a screenshot.
*/
public static void doScreenCapture(String desc) {
String testcaseID = TestMonitor.getInstance().getCurrentTestCaseID();
TraceHandler.trace(IRuntimePluginTraceOptions.WIDGET_SELECTION, "Creating screenshot (" + desc + ") for testcase: " + testcaseID);
ScreenCapture.createScreenCapture(testcaseID /*+ "_" + desc*/);
}
/**
* Debugging helper.
*/
class ShellStateDebuggingHelper {
/**
* Build a descrption of the current shells.
*/
String getShellStateDump() {
StringBuffer sb = new StringBuffer();
sb.append("Open Shells:").append("\n\n");
Shell[] shells = getDisplay().getShells();
for (int i = 0; i < shells.length; i++) {
sb.append(getState(shells[i])).append("\n");
}
return sb.toString();
}
String getState(Shell shell) {
Composite parent = getShellTester().getParent(shell);
return "Shell ("
+ getShellTester().getText(shell)
+ ") <"
+ shell.hashCode()
+ "> visible="
+ getShellTester().isVisible(shell)
+ " | modal="
+ isModal(shell)
+ " | parent=<"
+ ((parent == null) ? "null" : Integer.toString(parent
.hashCode())) + ">"
+ (getShellTester().isDisposed(shell) ? " [* disposed *]" : "");
}
}
}