blob: f83dd28819e6b9a6bae25bfc5de79af05f2a302c [file] [log] [blame]
/*****************************************************************************
* Copyright (c) 2014, 2016 CEA LIST, Christian W. Damus, and others.
*
* 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:
* Camille Letavernier (CEA LIST) camille.letavernier@cea.fr - Initial API and implementation
* Christian W. Damus (CEA) - add support for conditional tests
* Christian W. Damus (CEA) - bug 432813
* Christian W. Damus (CEA) - bug 434993
* Christian W. Damus (CEA) - bug 436047
* Christian W. Damus - bug 485156
*
*****************************************************************************/
package org.eclipse.papyrus.junit.framework.classification;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.eclipse.core.commands.operations.DefaultOperationHistory;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.viewers.BaseLabelProvider;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.papyrus.infra.tools.util.ListHelper;
import org.eclipse.papyrus.junit.framework.classification.rules.ConditionRule;
import org.eclipse.papyrus.junit.framework.classification.rules.Conditional;
import org.eclipse.papyrus.junit.framework.classification.rules.MemoryLeakRule;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
/**
* Internal implementation of the common classification-sensitive behaviour
* of the {@link ClassificationRunner} and {@link ClassificationRunnerWithParameters}
* test runners.
*
* @author Camille Letavernier
*/
class ClassificationRunnerImpl {
private final static long EVENT_LOOP_TIMEOUT = 2L * 60L * 1000L; // 2 minutes in millis
private final static long ONE_MB = 1024L * 1024L; // a megabyte, in bytes
private static final Supplier<TestRule> uiFlusherRuleSupplier = createUIFlusherRuleSupplier();
private final ThreadLocal<Object> preparedTest = new ThreadLocal<Object>();
private final Delegate delegate;
ClassificationRunnerImpl(Delegate delegate) throws InitializationError {
super();
this.delegate = delegate;
}
final void runChild(FrameworkMethod method, RunNotifier notifier) {
List<Annotation> allAnnotations = ListHelper.asList(method.getAnnotations());
allAnnotations.addAll(Arrays.asList(method.getMethod().getDeclaringClass().getAnnotations()));
if (ClassificationConfig.shouldRun(allAnnotations.toArray(new Annotation[allAnnotations.size()])) && conditionSatisfied(method)) {
delegate.runChild(method, notifier);
} else {
Description description = delegate.describeChild(method);
notifier.fireTestIgnored(description);
}
}
final Object createTest() throws Exception {
// Look for a prepared test instance
Object result = preparedTest.get();
if (result != null) {
// We won't need this test instance again
clearPreparedTest();
} else {
result = delegate.createTest();
}
return result;
}
final Object prepareTest() throws Exception {
// Prepare the test instance and stash it to return on the next invocation
Object result = delegate.createTest();
preparedTest.set(result);
return result;
}
final void clearPreparedTest() {
preparedTest.remove();
}
private boolean conditionSatisfied(FrameworkMethod method) {
boolean result = true;
// Does this test declare some precondition?
Conditional conditional = method.getAnnotation(Conditional.class);
if (conditional != null) {
try {
// We need the test instance to invoke the condition on it, so prepare it now
Object test = prepareTest();
result = ConditionRule.testCondition(method.getMethod().getDeclaringClass(), conditional, test);
} catch (Throwable t) {
// If we couldn't create the test, then we should just ignore it
result = false;
} finally {
if (!result) {
// We won't be running the test, so forget the prepared instance (if any)
clearPreparedTest();
}
}
}
return result;
}
List<TestRule> getTestRules(Object target) {
// MemoryLeakRules must be the outer-most rules, because leak assertions must only happen after all possible tear-down actions have run
return reorderForMemoryLeakRules(delegate.getTestRules(target));
}
private List<TestRule> reorderForMemoryLeakRules(List<TestRule> rules) {
// Quick scan for memory rules
if (!rules.isEmpty()) {
int memoryRuleCount = Iterables.size(Iterables.filter(rules, Predicates.instanceOf(MemoryLeakRule.class)));
if (memoryRuleCount > 0) {
// Bubble the memory rules to the end
int limit = rules.size() - memoryRuleCount;
for (int i = 0; i < limit; i++) {
if (rules.get(i) instanceof MemoryLeakRule) {
// Move the rule to the end and take a step back to get the next element
rules.add(rules.remove(i--));
}
}
}
}
return rules;
}
Statement classBlock(RunNotifier notifier) {
Statement result = delegate.classBlock(notifier);
// Wrap the class suite in a rule that flushes the UI thread to release memory referenced by UI runnables
TestRule uiFlusher = uiFlusherRuleSupplier.get();
if (uiFlusher != null) {
// This rule doesn't need any actual test description
result = uiFlusher.apply(result, Description.EMPTY);
}
return result;
}
private static Supplier<TestRule> createUIFlusherRuleSupplier() {
Supplier<TestRule> result = Suppliers.ofInstance(null);
try {
if (PlatformUI.isWorkbenchRunning()) {
result = Suppliers.memoize(new Supplier<TestRule>() {
@Override
public TestRule get() {
if (Display.getCurrent() != null) {
return new TestWatcher() {
@Override
protected void finished(Description description) {
final Display display = Display.getCurrent();
if (display == null) {
// Can't do UI manipulations and history listener hacking except on the UI thread
return;
}
flushUIEventQueue(display);
purgeZombieHistoryListeners();
clearDecorationScheduler();
}
};
}
return null;
}
});
}
} catch (LinkageError e) {
// Not running in Eclipse UI context. Fine
}
return result;
}
private static void flushUIEventQueue(Display display) {
long base = System.currentTimeMillis();
long timeout = EVENT_LOOP_TIMEOUT;
// Flush the UI thread's pending events
while (!display.isDisposed()) {
try {
if (!display.readAndDispatch()) {
break;
}
} catch (Exception e) {
// Ignore it
}
long now = System.currentTimeMillis();
if ((now - base) > timeout) {
// This seems to be taking a really long time. What's up?
base = now;
timeout = timeout * 3L / 2L; // Exponential back-off to avoid over-reporting
int freeMB = (int) (Runtime.getRuntime().freeMemory() / ONE_MB);
System.err.printf("========%nUI event queue clean-up seems to be running long.%nCurrent free memory: %d MB%n========%n%n", freeMB);
}
}
}
private static void purgeZombieHistoryListeners() {
// If there are no editors open any longer, then all of the action handlers currently
// listening to the operation history are leaked, so remove them. This ensures that we
// do not end up wasting time in notifying thousands of dead/broken/useless listeners
// every time a test case executes an operation on the history (which happens *a lot*)
IWorkbench bench = PlatformUI.getWorkbench();
IWorkbenchWindow window = (bench == null) ? null : bench.getActiveWorkbenchWindow();
if ((window == null) && (bench != null) && (bench.getWorkbenchWindowCount() > 0)) {
window = bench.getWorkbenchWindows()[0];
}
if (window != null && window.getActivePage().getEditorReferences().length == 0) {
final ListenerList historyListeners = OperationHistoryHelper.getOperationHistoryListeners();
final Object[] listeners = historyListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
if (OperationHistoryHelper.shouldRemoveHistoryListener(listeners[i])) {
historyListeners.remove(listeners[i]);
}
}
}
}
private static void clearDecorationScheduler() {
IWorkbench bench = PlatformUI.getWorkbench();
if (bench != null) {
IBaseLabelProvider bogusProvider = new BaseLabelProvider();
try {
// The DecoratorManager is a label-provider listener and
// it clears the scheduler on label-provider change events
((ILabelProviderListener) bench.getDecoratorManager()).labelProviderChanged(new LabelProviderChangedEvent(bogusProvider));
} finally {
bogusProvider.dispose();
}
}
}
//
// Nested types
//
static class OperationHistoryHelper {
static final Field listenersField;
static final Set<Class<?>> historyListenerClasses;
static {
try {
listenersField = DefaultOperationHistory.class.getDeclaredField("listeners");
listenersField.setAccessible(true);
historyListenerClasses = Sets.<Class<?>> newHashSet( //
Platform.getBundle("org.eclipse.gmf.runtime.diagram.ui.actions").loadClass("org.eclipse.gmf.runtime.diagram.ui.actions.internal.PropertyChangeContributionItem"), //
Platform.getBundle("org.eclipse.ui.workbench").loadClass("org.eclipse.ui.operations.OperationHistoryActionHandler$HistoryListener"));
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
static ListenerList getOperationHistoryListeners() {
try {
return (ListenerList) listenersField.get(PlatformUI.getWorkbench().getOperationSupport().getOperationHistory());
} catch (Exception e) {
org.junit.Assert.fail(e.getLocalizedMessage());
return null; // Unreachable
}
}
static boolean shouldRemoveHistoryListener(Object listener) {
boolean result = historyListenerClasses.contains(listener.getClass().getName());
if (!result) {
// Maybe it's a subclass
for (Class<?> next : historyListenerClasses) {
if (next.isInstance(listener)) {
// Remember this
historyListenerClasses.add(listener.getClass());
result = true;
break;
}
}
}
return result;
}
}
/**
* Protocol for a delegate that provides the default test framework behaviour
* for the classification runner. These methods are as specified by the
* corresponding APIs of the {@link BlockJUnit4ClassRunner} class.
*/
interface Delegate {
void runChild(FrameworkMethod method, RunNotifier notifier);
Description describeChild(FrameworkMethod method);
Object createTest() throws Exception;
List<TestRule> getTestRules(Object target);
Statement classBlock(RunNotifier notifier);
}
}