diff options
Diffstat (limited to 'tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/classification/ClassificationRunnerImpl.java')
-rw-r--r-- | tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/classification/ClassificationRunnerImpl.java | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/classification/ClassificationRunnerImpl.java b/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/classification/ClassificationRunnerImpl.java new file mode 100644 index 00000000000..f83dd28819e --- /dev/null +++ b/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/classification/ClassificationRunnerImpl.java @@ -0,0 +1,346 @@ +/***************************************************************************** + * 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); + } +} |