/******************************************************************************* * Copyright Technophobia Ltd 2012 * * This file is part of the Substeps Eclipse Plugin. * * The Substeps Eclipse Plugin is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License v1.0. * * The Substeps Eclipse Plugin is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Eclipse Public License for more details. * * You should have received a copy of the Eclipse Public License * along with the Substeps Eclipse Plugin. If not, see <http://www.eclipse.org/legal/epl-v10.html>. ******************************************************************************/ package com.technophobia.substeps.junit.ui; import java.io.File; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.ListenerList; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.core.ILaunchesListener2; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import com.technophobia.eclipse.launcher.config.SubstepsLaunchConfigurationConstants; import com.technophobia.eclipse.transformer.Callback1; import com.technophobia.substeps.FeatureRunnerPlugin; import com.technophobia.substeps.model.RemoteTestRunnerClient; import com.technophobia.substeps.model.SubstepsRunListener; import com.technophobia.substeps.model.SubstepsSessionListener; import com.technophobia.substeps.model.TestRunListenerAdapter; import com.technophobia.substeps.model.serialize.SubstepsModelExporter; import com.technophobia.substeps.model.structure.IncompleteParentItem; import com.technophobia.substeps.model.structure.LinkedParentItemManager; import com.technophobia.substeps.model.structure.PredicatedLinkedParentItemManager; import com.technophobia.substeps.model.structure.Result; import com.technophobia.substeps.model.structure.Status; import com.technophobia.substeps.model.structure.SubstepsTestElement; import com.technophobia.substeps.model.structure.SubstepsTestElementFactory; import com.technophobia.substeps.model.structure.SubstepsTestLeafElement; import com.technophobia.substeps.model.structure.SubstepsTestParentElement; import com.technophobia.substeps.model.structure.SubstepsTestRootElement; import com.technophobia.substeps.supplier.Supplier; import com.technophobia.substeps.supplier.Transformer; public class SubstepsRunSessionImpl implements SubstepsRunSession, TestRunStats { /** * The launch, or <code>null</code> iff this session was run externally. */ private final ILaunch launch; private final String testRunName; /** * Java project, or <code>null</code>. */ private final IJavaProject project; private final String testRunnerKind; /** * Test runner client or <code>null</code>. */ private RemoteTestRunnerClient testRunnerClient; private final ListenerList sessionListeners; /** * The model root, or <code>null</code> if swapped to disk. */ private SubstepsTestRootElement testRoot; /** * The test run session's cached result, or <code>null</code> if * <code>fTestRoot != null</code>. */ private Result testResult; /** * Map from testId to testElement. */ private HashMap<String, SubstepsTestElement> idToTest; /** * The Parent items for which additional children are expected. */ private final LinkedParentItemManager<IncompleteParentItem> incompleteParentItems; /** * Suite for unrooted test case elements, or <code>null</code>. */ private SubstepsTestParentElement unrootedSuite; /** * Number of tests started during this test run. */ volatile int startedCount; /** * Number of tests ignored during this test run. */ volatile int ignoredCount; /** * Number of errors during this test run. */ volatile int errorCount; /** * Number of failures during this test run. */ volatile int failureCount; /** * Total number of tests to run. */ volatile int totalCount; /** * <ul> * <li>If > 0: Start time in millis</li> * <li>If < 0: Unique identifier for imported test run</li> * <li>If = 0: Session not started yet</li> * </ul> */ volatile long startTime; volatile boolean isRunning; volatile boolean fIsStopped; private final SubstepsTestElementFactory testElementFactory; /** * Creates a test run session. * * @param testRunName * name of the test run * @param project * may be <code>null</code> */ public SubstepsRunSessionImpl(final String testRunName, final SubstepsTestElementFactory testElementFactory, final IJavaProject project) { // TODO: check assumptions about non-null fields this.testElementFactory = testElementFactory; this.launch = null; this.project = project; this.startTime = -System.currentTimeMillis(); Assert.isNotNull(testRunName); this.testRunName = testRunName; testRunnerKind = null; testRoot = new SubstepsTestRootElement(this); idToTest = new HashMap<String, SubstepsTestElement>(); this.incompleteParentItems = new PredicatedLinkedParentItemManager<IncompleteParentItem>(testRootSupplier(), decrementRemainingChildItemsCallback(), checkRemainingChildItemsPredicate()); testRunnerClient = null; sessionListeners = new ListenerList(); } public SubstepsRunSessionImpl(final ILaunch launch, final SubstepsTestElementFactory testElementFactory, final IJavaProject project, final int port) { this.testElementFactory = testElementFactory; Assert.isNotNull(launch); this.launch = launch; this.project = project; final ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration(); if (launchConfiguration != null) { testRunName = launchConfiguration.getName(); testRunnerKind = testRunnerKind(launchConfiguration); } else { testRunName = project.getElementName(); testRunnerKind = null; } testRoot = new SubstepsTestRootElement(this); idToTest = new HashMap<String, SubstepsTestElement>(); this.incompleteParentItems = new PredicatedLinkedParentItemManager<IncompleteParentItem>(testRootSupplier(), decrementRemainingChildItemsCallback(), checkRemainingChildItemsPredicate()); testRunnerClient = new RemoteTestRunnerClient(); testRunnerClient.startListening(new SubstepsRunListener[] { new TestSessionNotifier() }, port); final ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); launchManager.addLaunchListener(new ILaunchesListener2() { @Override public void launchesTerminated(final ILaunch[] launches) { if (Arrays.asList(launches).contains(launch)) { if (testRunnerClient != null) { testRunnerClient.stopWaiting(); } launchManager.removeLaunchListener(this); } } @Override public void launchesRemoved(final ILaunch[] launches) { if (Arrays.asList(launches).contains(launch)) { if (testRunnerClient != null) { testRunnerClient.stopWaiting(); } launchManager.removeLaunchListener(this); } } @Override public void launchesChanged(final ILaunch[] launches) { // No-op } @Override public void launchesAdded(final ILaunch[] launches) { // No-op } }); sessionListeners = new ListenerList(); addTestSessionListener(new TestRunListenerAdapter(this)); } private String testRunnerKind(final ILaunchConfiguration launchConfiguration) { try { return launchConfiguration.getAttribute(SubstepsLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND, (String) null); } catch (final CoreException e) { return null; } } @Override public void reset() { startedCount = 0; failureCount = 0; errorCount = 0; ignoredCount = 0; totalCount = 0; testRoot = new SubstepsTestRootElement(this); testResult = null; idToTest = new HashMap<String, SubstepsTestElement>(); } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.model.ITestElement#getTestResult(boolean) */ @Override public Result getTestResult(final boolean includeChildren) { if (testRoot != null) { return testRoot.getTestResult(true); } return testResult; } @Override public int getChildCount() { return getTestRoot().getChildCount(); } @Override public SubstepsTestElement[] getChildren() { return getTestRoot().getChildren(); } @Override public FailureTrace getFailureTrace() { return null; } @Override public SubstepsRunSession getSubstepsRunSession() { return this; } @Override public synchronized SubstepsTestRootElement getTestRoot() { swapIn(); // TODO: TestRoot should stay (e.g. for // getTestRoot().getStatus()) return testRoot; } /* * @see org.eclipse.jdt.junit.model.ITestRunSession#getJavaProject() */ @Override public IJavaProject getLaunchedProject() { return project; } @Override public String getTestRunnerKind() { return testRunnerKind; } /** * @return the launch, or <code>null</code> iff this session was run * externally */ @Override public ILaunch getLaunch() { return launch; } @Override public String getTestRunName() { return testRunName; } @Override public int getErrorCount() { return errorCount; } @Override public int getFailureCount() { return failureCount; } @Override public int getStartedCount() { return startedCount; } @Override public int getIgnoredCount() { return ignoredCount; } @Override public int getTotalCount() { return totalCount; } @Override public long getStartTime() { return startTime; } @Override public TestRunState getState() { if (isRunning()) { return TestRunState.IN_PROGRESS; } else if (isStopped()) { return TestRunState.STOPPED; } return TestRunState.COMPLETE; } @Override public boolean hasErrorsOrFailures() { return getFailureCount() > 0 || getErrorCount() > 0; } /** * @return <code>true</code> iff the session has been stopped or terminated */ @Override public boolean isStopped() { return fIsStopped; } @Override public synchronized void addTestSessionListener(final SubstepsSessionListener listener) { swapIn(); sessionListeners.add(listener); } @Override public void removeTestSessionListener(final SubstepsSessionListener listener) { sessionListeners.remove(listener); } @Override public synchronized void swapOut() { if (testRoot == null) return; if (isRunning() || isStarting() || isKeptAlive()) return; final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { final SubstepsSessionListener registered = (SubstepsSessionListener) listeners[i]; if (!registered.acceptsSwapToDisk()) return; } try { final File swapFile = getSwapFile(); SubstepsModelExporter.exportTestRunSession(this, swapFile); testResult = testRoot.getTestResult(true); testRoot = null; testRunnerClient = null; idToTest = new HashMap<String, SubstepsTestElement>(); incompleteParentItems.reset(); unrootedSuite = null; } catch (final IllegalStateException e) { FeatureRunnerPlugin.log(e); } catch (final CoreException e) { FeatureRunnerPlugin.log(e); } } @Override public boolean isStarting() { return getStartTime() == 0 && launch != null && !launch.isTerminated(); } @Override public void removeSwapFile() { final File swapFile = getSwapFile(); if (swapFile.exists()) swapFile.delete(); } private File getSwapFile() throws IllegalStateException { final File historyDir = FeatureRunnerPlugin.instance().getHistoryDirectory(); final String isoTime = new SimpleDateFormat("yyyyMMdd-HHmmss.SSS").format(new Date(getStartTime())); //$NON-NLS-1$ final String swapFileName = isoTime + ".xml"; //$NON-NLS-1$ return new File(historyDir, swapFileName); } public synchronized void swapIn() { if (testRoot != null) return; // throw new UnsupportedOperationException("Import from disk not supported"); /* * try { JUnitModel.importIntoTestRunSession(getSwapFile(), this); } * catch (final IllegalStateException e) { JUnitCorePlugin.log(e); * testRoot = new SubstepsTestRootElement(this); testResult = null; } * catch (final CoreException e) { JUnitCorePlugin.log(e); testRoot = * new SubstepsTestRootElement(this); testResult = null; } */ } @Override public void stopTestRun() { if (isRunning() || !isKeptAlive()) fIsStopped = true; if (testRunnerClient != null) testRunnerClient.stopTest(); } /** * @return <code>true</code> iff the runtime VM of this test session is * still alive */ @Override public boolean isKeptAlive() { if (testRunnerClient != null && launch != null && testRunnerClient.isRunning() && ILaunchManager.DEBUG_MODE.equals(launch.getLaunchMode())) { final ILaunchConfiguration config = launch.getLaunchConfiguration(); try { return config != null && config.getAttribute(SubstepsLaunchConfigurationConstants.ATTR_KEEPRUNNING, false); } catch (final CoreException e) { return false; } } return false; } /** * @return <code>true</code> iff this session has been started, but not * ended nor stopped nor terminated */ @Override public boolean isRunning() { return isRunning; } /** * Reruns the given test method. * * @param testId * test id * @param className * test class name * @param testName * test method name * @param launchMode * launch mode, see {@link ILaunchManager} * @param buildBeforeLaunch * whether a build should be done before launch * @return <code>false</code> iff the rerun could not be started * @throws CoreException * if the launch fails */ @Override public boolean rerunTest(final String testId, final String className, final String testName, final String launchMode, final boolean buildBeforeLaunch) throws CoreException { if (isKeptAlive()) { final Status status = ((SubstepsTestLeafElement) getTestElement(testId)).getStatus(); if (status == Status.ERROR) { errorCount--; } else if (status == Status.FAILURE) { failureCount--; } testRunnerClient.rerunTest(testId, className, testName); return true; } else if (launch != null) { // run the selected test using the previous launch configuration final ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration(); if (launchConfiguration != null) { String name = className; if (testName != null) name += "." + testName; //$NON-NLS-1$ final String configName = MessageFormat.format( SubstepsFeatureMessages.SubstepsFeatureTestRunnerViewPart_configName, name); final ILaunchConfigurationWorkingCopy tmp = launchConfiguration.copy(configName); // fix for bug: 64838 junit view run single test does not use // correct class [JUnit] tmp.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, className); // reset the container tmp.setAttribute(SubstepsLaunchConfigurationConstants.ATTR_TEST_CONTAINER, ""); //$NON-NLS-1$ if (testName != null) { tmp.setAttribute(SubstepsLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, testName); // String args= "-rerun "+testId; // tmp.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, // args); } tmp.launch(launchMode, null, buildBeforeLaunch); return true; } } return false; } @Override public SubstepsTestElement getTestElement(final String id) { return idToTest.get(id); } private SubstepsTestElement addTreeEntry(final String treeEntry) { final SubstepsTestElement testElement = testElementFactory .createForTestEntryString(treeEntry, parentSupplier()); if (!incompleteParentItems.isEmpty()) { incompleteParentItems.processOutstandingChild(); } if (testElement instanceof SubstepsTestParentElement) { final SubstepsTestParentElement parentElement = (SubstepsTestParentElement) testElement; if (parentElement.getChildCount() > 0) incompleteParentItems.addNode(new IncompleteParentItem(parentElement, parentElement.getChildCount())); } idToTest.put(testElement.getId(), testElement); return testElement; // SubstepsTestElement testElement = // testElementFactory.createForTestEntryString(treeEntry); // if(testElement instanceof SubstepsTestParentElement){ // SubstepsTestParentElement parent = (SubstepsTestParentElement) // testElement; // if(parent.getchil) // } // // if (incompleteTestSuites.isEmpty()) { // return createTestElement(testRoot, id, testName, isSuite, testCount); // } else { // final int suiteIndex = incompleteTestSuites.size() - 1; // final IncompleteTestSuite openSuite = // incompleteTestSuites.get(suiteIndex); // openSuite.outstandingChildren--; // if (openSuite.outstandingChildren <= 0) // incompleteTestSuites.remove(suiteIndex); // return createTestElement(openSuite.testSuiteElement, id, testName, // isSuite, testCount); // } } /** * An {@link SubstepsRunListener} that listens to events from the * {@link RemoteTestRunnerClient} and translates them into high-level model * events (broadcasted to {@link SubstepsSessionListener}s). */ private class TestSessionNotifier implements SubstepsRunListener { @Override public void testRunStarted(final int testCount) { incompleteParentItems.reset(); startedCount = 0; ignoredCount = 0; failureCount = 0; errorCount = 0; totalCount = testCount; startTime = System.currentTimeMillis(); isRunning = true; final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).sessionStarted(); } } @Override public void testRunEnded(final long elapsedTime) { isRunning = false; final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).sessionEnded(elapsedTime); } } @Override public void testRunStopped(final long elapsedTime) { isRunning = false; fIsStopped = true; final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).sessionStopped(elapsedTime); } } @Override public void testRunTerminated() { isRunning = false; fIsStopped = true; final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).sessionTerminated(); } } @Override public void testTreeEntry(final String description) { final SubstepsTestElement testElement = addTreeEntry(description); final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).testAdded(testElement); } } private SubstepsTestElement createUnrootedTestElement(final String testId, final String testName) { final SubstepsTestParentElement unrooted = getUnrootedSuite(); final SubstepsTestElement testElement = testElementFactory.createTestElement(unrooted, testId, testName, false, 1); final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).testAdded(testElement); } return testElement; } private SubstepsTestParentElement getUnrootedSuite() { if (unrootedSuite == null) { unrootedSuite = (SubstepsTestParentElement) testElementFactory.createTestElement(testRoot, "-2", SubstepsFeatureMessages.SubstepsRunSession_unrootedTests, true, 0); //$NON-NLS-1$ } return unrootedSuite; } @Override public void testStarted(final String testId, final String testName) { if (startedCount == 0) { final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).runningBegins(); } } SubstepsTestElement testElement = getTestElement(testId); if (testElement == null) { testElement = createUnrootedTestElement(testId, testName); } if (testElement instanceof SubstepsTestLeafElement) { final SubstepsTestLeafElement testCaseElement = (SubstepsTestLeafElement) testElement; setStatus(testCaseElement, Status.RUNNING); startedCount++; final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).testStarted(testCaseElement); } } } @Override public void testEnded(final String testId, final String testName) { SubstepsTestElement testElement = getTestElement(testId); if (testElement == null) { testElement = createUnrootedTestElement(testId, testName); } if (testElement instanceof SubstepsTestLeafElement) { final SubstepsTestLeafElement testCaseElement = (SubstepsTestLeafElement) testElement; if (testName.startsWith("@Ignore: ")) { testCaseElement.setIgnored(true); ignoredCount++; } if (testCaseElement.getStatus() == Status.RUNNING) setStatus(testCaseElement, Status.OK); final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).testEnded(testCaseElement); } } } /* * (non-Javadoc) * * @see * org.eclipse.jdt.internal.junit.model.ITestRunListener2#testFailed * (int, java.lang.String, java.lang.String, java.lang.String, * java.lang.String, java.lang.String) */ @Override public void testFailed(final Status status, final String testId, final String testName, final String trace, final String expected, final String actual) { SubstepsTestElement testElement = getTestElement(testId); if (testElement == null) { testElement = createUnrootedTestElement(testId, testName); } registerTestFailureStatus(testElement, status, trace, expected, actual); final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((SubstepsSessionListener) listeners[i]).testFailed(testElement, status, trace, expected, actual); } } /* * (non-Javadoc) * * @see * org.eclipse.jdt.internal.junit.model.ITestRunListener2#testReran( * java.lang.String, java.lang.String, java.lang.String, int, * java.lang.String, java.lang.String, java.lang.String) */ @Override public void testReran(final String testId, final String className, final String testName, final Status status, final String trace, final String expectedResult, final String actualResult) { SubstepsTestElement testElement = getTestElement(testId); if (testElement == null) { testElement = createUnrootedTestElement(testId, testName); } if (testElement instanceof SubstepsTestLeafElement) { final SubstepsTestLeafElement testCaseElement = (SubstepsTestLeafElement) testElement; registerTestFailureStatus(testElement, status, trace, expectedResult, actualResult); final Object[] listeners = sessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { // TODO: post old & new status? ((SubstepsSessionListener) listeners[i]).testReran(testCaseElement, status, trace, expectedResult, actualResult); } } } @Override public void sessionLaunched(final SubstepsRunSession substepsRunSession) { // TODO Auto-generated method stub } @Override public void sessionStarted(final SubstepsRunSession session) { // TODO Auto-generated method stub } @Override public void sessionFinished(final SubstepsRunSession session) { // TODO Auto-generated method stub } @Override public void testCaseStarted(final SubstepsTestLeafElement testCaseElement) { // TODO Auto-generated method stub } @Override public void testCaseFinished(final SubstepsTestLeafElement testCaseElement) { // TODO Auto-generated method stub } } @Override public void registerTestFailureStatus(final SubstepsTestElement testElement, final Status status, final String trace, final String expected, final String actual) { testElement.setStatus(status, trace, expected, actual); if (status.isError()) { errorCount++; } else if (status.isFailure()) { failureCount++; } } @Override public void registerTestEnded(final SubstepsTestElement testElement, final boolean completed) { if (testElement instanceof SubstepsTestLeafElement) { totalCount++; if (!completed) { return; } startedCount++; if (((SubstepsTestLeafElement) testElement).isIgnored()) { ignoredCount++; } if (!testElement.getStatus().isErrorOrFailure()) setStatus(testElement, Status.OK); } } private void setStatus(final SubstepsTestElement testElement, final Status status) { testElement.setStatus(status); } @Override public SubstepsTestElement[] getAllFailedTestElements() { final ArrayList<SubstepsTestElement> failures = new ArrayList<SubstepsTestElement>(); addFailures(failures, getTestRoot()); return failures.toArray(new SubstepsTestElement[failures.size()]); } private void addFailures(final ArrayList<SubstepsTestElement> failures, final SubstepsTestElement testElement) { final Result result = testElement.getTestResult(true); if (result == Result.ERROR || result == Result.FAILURE) { failures.add(testElement); } if (testElement instanceof SubstepsTestParentElement) { final SubstepsTestParentElement testSuiteElement = (SubstepsTestParentElement) testElement; final SubstepsTestElement[] children = testSuiteElement.getChildren(); for (int i = 0; i < children.length; i++) { addFailures(failures, children[i]); } } } private Transformer<IncompleteParentItem, Boolean> checkRemainingChildItemsPredicate() { return new Transformer<IncompleteParentItem, Boolean>() { @Override public Boolean from(final IncompleteParentItem from) { return Boolean.valueOf(!from.hasOutstandingChildren()); } }; } private Callback1<IncompleteParentItem> decrementRemainingChildItemsCallback() { return new Callback1<IncompleteParentItem>() { @Override public void callback(final IncompleteParentItem t) { t.decrementOutstandingChildren(); } }; } private Supplier<IncompleteParentItem> testRootSupplier() { return new Supplier<IncompleteParentItem>() { @Override public IncompleteParentItem get() { return new IncompleteParentItem(testRoot, testRoot.getChildCount()); } }; } private Supplier<SubstepsTestParentElement> parentSupplier() { return new Supplier<SubstepsTestParentElement>() { @Override public SubstepsTestParentElement get() { return incompleteParentItems.get().getParentElement(); } }; } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.model.ITestElement#getElapsedTimeInSeconds() */ @Override public double getElapsedTimeInSeconds() { if (testRoot == null) return Double.NaN; return testRoot.getElapsedTimeInSeconds(); } @Override public String toString() { return testRunName + " " + DateFormat.getDateTimeInstance().format(new Date(startTime)); //$NON-NLS-1$ } }