blob: 333ee659d2aab6dc93c1b1bcb97463b4dd2d551d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.unittest.internal.model;
import java.text.DateFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import org.eclipse.unittest.internal.UnitTestPlugin;
import org.eclipse.unittest.internal.launcher.TestViewSupportRegistry;
import org.eclipse.unittest.launcher.ITestRunnerClient;
import org.eclipse.unittest.model.ITestCaseElement;
import org.eclipse.unittest.model.ITestElement;
import org.eclipse.unittest.model.ITestRunSession;
import org.eclipse.unittest.model.ITestSuiteElement;
import org.eclipse.unittest.ui.ITestViewSupport;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.ILaunchesListener2;
import org.eclipse.debug.core.Launch;
import org.eclipse.debug.core.model.ISourceLocator;
/**
* A test run session holds all information about a test run, i.e. launch
* configuration, launch, test tree (including results).
*/
public class TestRunSession extends TestSuiteElement implements ITestRunSession, ITestRunSessionReport {
/**
* The launch, or <code>null</code> iff this session was run externally.
*/
private final ILaunch fLaunch;
private final String fTestRunName;
private final ITestViewSupport fTestRunnerSupport;
/**
* Test runner client or <code>null</code>.
*/
private ITestRunnerClient fTestRunnerClient;
private final ListenerList<ITestSessionListener> fSessionListeners;
private final TestSessionNotifier fSessionNotifier = new TestSessionNotifier();
/**
* The test run session's cached result, or <code>null</code> if
* <code>fTestRoot != null</code>.
*/
private Result fTestResult;
/**
* Map from testId to testElement.
*/
private HashMap<String, TestElement> fIdToTest;
volatile Instant fStartTime;
volatile Integer fPredefinedTestCount;
volatile boolean fIsAborted;
private Integer predefinedTestCount;
private boolean completedOrAborted;
/**
* Constructs a test run session object.
*
* @param testRunName name of the test run
* @param startTime a start time of a test run
* @param launchConfiguration a launch configuration for a test run
*/
public TestRunSession(String testRunName, Instant startTime, ILaunchConfiguration launchConfiguration) {
super(null, "-1", testRunName, null, null, null); //$NON-NLS-1$
// TODO: check assumptions about non-null fields
fLaunch = new NoopLaunch(launchConfiguration, ILaunchManager.RUN_MODE, null);
fTestRunnerSupport = TestViewSupportRegistry.newTestRunnerViewSupport(launchConfiguration).orElse(null);
Assert.isNotNull(testRunName);
fTestRunName = testRunName;
fIdToTest = new HashMap<>();
fTestRunnerClient = null;
fStartTime = startTime;
fSessionListeners = new ListenerList<>();
}
/**
* Constructs a test run session object from a launch.
*
* @param launch an {@link ILaunch} object
*/
public TestRunSession(ILaunch launch) {
super(null, "-1", "<TestRunSession>", null, null, null); //$NON-NLS-1$ //$NON-NLS-2$
Assert.isNotNull(launch);
fLaunch = launch;
ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
if (launchConfiguration != null) {
fTestRunName = launchConfiguration.getName();
fTestRunnerSupport = TestViewSupportRegistry.newTestRunnerViewSupport(launchConfiguration).orElse(null);
} else {
fTestRunName = "<TestRunSession>"; //$NON-NLS-1$
fTestRunnerSupport = null;
}
fIdToTest = new HashMap<>();
if (fTestRunnerSupport != null) {
fTestRunnerClient = fTestRunnerSupport.newTestRunnerClient(this);
}
final ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager();
launchManager.addLaunchListener(new ILaunchesListener2() {
@Override
public void launchesTerminated(ILaunch[] launches) {
if (Arrays.asList(launches).contains(fLaunch)) {
launchManager.removeLaunchListener(this);
}
}
@Override
public void launchesRemoved(ILaunch[] launches) {
if (Arrays.asList(launches).contains(fLaunch)) {
launchManager.removeLaunchListener(this);
}
}
@Override
public void launchesChanged(ILaunch[] launches) {
// do nothing
}
@Override
public void launchesAdded(ILaunch[] launches) {
// do nothing
}
});
fSessionListeners = new ListenerList<>();
setStatus(Status.RUNNING);
addTestSessionListener(new TestRunListenerAdapter(this));
fTestRunnerClient.startMonitoring();
}
/**
* Resets a test run session object (used when re-creating a test run session
* from an XML file(
*
* TODO Consider removal as it's only use in XML parsing
*/
public void reset() {
fTestResult = null;
fIdToTest = new HashMap<>();
}
@Override
public ProgressState getProgressState() {
if (isRunning()) {
return ProgressState.RUNNING;
}
if (isStopped()) {
return ProgressState.ABORTED;
}
return ProgressState.COMPLETED;
}
@Override
public FailureTrace getFailureTrace() {
return null;
}
@Override
public TestSuiteElement getParentContainer() {
return null;
}
@Override
public TestRunSession getTestRunSession() {
return this;
}
/**
* Returns the Test Runner View Support for which this test run session has been
* launched, or <code>null</code> if not available.
*
* @return the test runner view support, or <code>null</code> is not available.
*/
public ITestViewSupport getTestViewSupport() {
return fTestRunnerSupport;
}
/**
* @return the launch, or <code>null</code> iff this session was run externally
*/
@Override
public ILaunch getLaunch() {
return fLaunch;
}
@Override
public String getTestRunName() {
return fTestRunName;
}
@Override
public int getCurrentErrorCount() {
return getChildren().stream().mapToInt(TestElement::getCurrentErrorCount).sum();
}
@Override
public int getCurrentFailureCount() {
return getChildren().stream().mapToInt(TestElement::getCurrentFailureCount).sum();
}
@Override
public int getCurrentAssumptionFailureCount() {
return getChildren().stream().mapToInt(TestElement::getCurrentAssumptionFailureCount).sum();
}
@Override
public int getCurrentIgnoredCount() {
return getChildren().stream().mapToInt(TestElement::getCurrentIgnoredCount).sum();
}
/**
* Returns start time for a run session
*
* @return an {@link Instant} object indicating a test run session start time
*/
public Instant getStartTime() {
return fStartTime;
}
/**
* Indicates if the test run session has been stopped or terminated
*
* @return <code>true</code> if the session has been stopped or terminated,
* otherwise returns <code>false</code>
*/
@Override
public boolean isStopped() {
return fIsAborted;
}
/**
* Adds an {@link ITestSessionListener} listener
*
* @param listener an {@link ITestSessionListener} object
*/
public synchronized void addTestSessionListener(ITestSessionListener listener) {
fSessionListeners.add(listener);
}
/**
* Removes an {@link ITestSessionListener} listener
*
* @param listener an {@link ITestSessionListener} object
*/
public void removeTestSessionListener(ITestSessionListener listener) {
fSessionListeners.remove(listener);
}
@Override
public boolean isStarting() {
return getStartTime() == null && fLaunch != null && !fLaunch.isTerminated();
}
/**
* Forces a test run session to abort its execution
*/
public void abortTestRun() {
fIsAborted = true;
if (fTestRunnerClient != null) {
fTestRunnerClient.stopTest();
fTestRunnerClient.stopMonitoring();
}
}
@Override
public boolean isRunning() {
return getStartTime() != null && fTestRunnerClient != null && !completedOrAborted;
}
@Override
public TestElement getTestElement(String id) {
return fIdToTest.get(id);
}
private TestElement addTreeEntry(String id, String testName, boolean isSuite, Integer testCount,
boolean isDynamicTest, TestSuiteElement parent, String displayName, String data) {
return createTestElement(parent != null ? parent : this, id, testName, isSuite, testCount, isDynamicTest,
displayName, data);
}
/**
* Creates a test element, either {@link ITestSuiteElement} or
* {@link ITestCaseElement} instance, depending on the arguments.
*
* @param parent a parent test suite element
* @param id an identifier of the test element
* @param testName a name of the test element
* @param isSuite a flag indicating if the test element should be
* represented by a test suite element
* @param testCount a number of predefined test cases in case of test suite
* element
* @param isDynamicTest a flag indicating that test suite is dynamic (that
* doesn't have predefined tests)
* @param displayName a display name for the test element
* @param data some test runner specific data, can be <code>null</code>
* @return a created {@link ITestSuiteElement} or {@link ITestCaseElement}
* instance
*/
public TestElement createTestElement(TestSuiteElement parent, String id, String testName, boolean isSuite,
Integer testCount, boolean isDynamicTest, String displayName, String data) {
TestElement testElement;
if (isSuite) {
TestSuiteElement testSuiteElement = new TestSuiteElement(parent != null ? parent : this, id, testName,
testCount, displayName, data);
testElement = testSuiteElement;
} else {
testElement = new TestCaseElement(parent != null ? parent : this, id, testName, displayName, isDynamicTest,
data);
}
fIdToTest.put(id, testElement);
return testElement;
}
private final class NoopLaunch extends Launch {
private NoopLaunch(ILaunchConfiguration launchConfiguration, String mode, ISourceLocator locator) {
super(launchConfiguration, mode, locator);
}
@Override
public boolean isTerminated() {
return true;
}
@Override
public boolean isDisconnected() {
return true;
}
}
/**
* Listens to events from the and translates {@link ITestRunnerClient} them into
* high-level model events (broadcasted to {@link ITestSessionListener}s).
*/
private class TestSessionNotifier {
private boolean firstStart;
/**
* Notifies on a test run started
*
* @param testCount number of tests in this run
*/
public void testRunStarted(Integer testCount) {
fStartTime = Instant.now();
fPredefinedTestCount = testCount;
for (ITestSessionListener listener : fSessionListeners) {
listener.sessionStarted();
}
}
/**
* Notifies on a test run ended
*
* @param duration a duration of this test run
*/
public void testRunEnded(Duration duration) {
for (ITestSessionListener listener : fSessionListeners) {
listener.sessionCompleted(duration);
}
}
/**
* Notifies on a test run stopped (aborted)
*
* @param duration a duration of this test run
*/
public void testRunStopped(Duration duration) {
fIsAborted = true;
for (ITestSessionListener listener : fSessionListeners) {
listener.sessionAborted(duration);
}
}
/**
* Notifies on a test tree entry is created
*
* @param testId an identifier of a test entry
* @param testName a name of test
* @param isSuite <code>true</code> indicates that a test entry is a test
* suite, <code>false</code> - a test case
* @param testCount a number of tests in a test suite or <code>null</code>
* @param isDynamicTest indicates if a test is a dynamic test (doesn't have a
* predefined number of tests)
* @param parent a parent test suite element
* @param displayName a display name for the test
* @param uniqueId an unique identifier of test entry or <code>null</code>
* @return an {@link ITestElement} object instance
*/
public ITestElement testTreeEntry(String testId, String testName, boolean isSuite, Integer testCount,
boolean isDynamicTest, ITestSuiteElement parent, String displayName, String uniqueId) {
ITestElement testElement = addTreeEntry(testId, testName, isSuite, testCount, isDynamicTest,
(TestSuiteElement) parent, displayName, uniqueId);
for (ITestSessionListener listener : fSessionListeners) {
listener.testAdded(testElement);
}
return testElement;
}
/**
* Notifies on a test started
*
* @param test a test element object
*/
public void testStarted(ITestElement test) {
if (!(test instanceof TestCaseElement)) {
return;
}
if (firstStart) {
for (ITestSessionListener listener : fSessionListeners) {
listener.runningBegins();
}
firstStart = false;
}
setStatus(test, Status.RUNNING);
for (ITestSessionListener listener : fSessionListeners) {
listener.testStarted((ITestCaseElement) test);
}
}
/**
* Notifies on a test ended
*
* @param testElement a test element object
* @param isIgnored indicates a skipped (not run) test element
*/
public void testEnded(ITestElement testElement, boolean isIgnored) {
if (testElement == null) {
return;
}
if (!(testElement instanceof TestCaseElement)) {
if (isIgnored) {
((TestElement) testElement).setAssumptionFailed(true);
setStatus(testElement, Status.OK);
} else {
logUnexpectedTest(testElement.getId(), testElement);
}
return;
}
TestCaseElement testCaseElement = (TestCaseElement) testElement;
if (isIgnored) {
testCaseElement.setIgnored(true);
}
if (testCaseElement.getStatus() == Status.RUNNING)
setStatus(testCaseElement, Status.OK);
for (ITestSessionListener listener : fSessionListeners) {
listener.testEnded(testCaseElement);
}
}
/**
* Notifies on a failed test element
*
* @param testElement a failed test element
* @param status a result status of test execution
* @param isAssumptionFailed indicates if the failure is an assumption failure
* @param trace a failure trace
*/
public void testFailed(ITestElement testElement, Result status, boolean isAssumptionFailed,
FailureTrace trace) {
if (testElement == null) {
return;
}
if (isAssumptionFailed) {
((TestElement) testElement).setAssumptionFailed(true);
status = Result.OK;
}
registerTestFailureStatus((TestElement) testElement, status, trace);
for (ITestSessionListener listener : fSessionListeners) {
listener.testFailed(testElement, status, trace);
}
}
private void logUnexpectedTest(String testId, ITestElement testElement) {
UnitTestPlugin
.log(new Exception("Unexpected TestElement type for testId '" + testId + "': " + testElement)); //$NON-NLS-1$ //$NON-NLS-2$
}
}
/**
* Registers a failure status for a test element
*
* @param testElement a test element
* @param status a result of test execution
* @param failureTrace a failure trace
*/
public void registerTestFailureStatus(TestElement testElement, Result status, FailureTrace failureTrace) {
testElement.setStatus(Status.fromResult(status), failureTrace);
}
/**
* Registers an ended test element
*
* @param testElement a test element
* @param completed <code>true</code> indicates that the test was completed.
* <code>false</code> otherwise
*/
public void registerTestEnded(TestElement testElement, boolean completed) {
if (testElement instanceof TestCaseElement) {
if (!completed) {
return;
}
TestCaseElement testCaseElement = (TestCaseElement) testElement;
if (!testCaseElement.getStatus().isErrorOrFailure())
setStatus(testElement, Status.OK);
}
}
/**
* Sets a {@link Status} for a test element
*
* @param testElement a test element
* @param status an execution status
*/
private void setStatus(ITestElement testElement, Status status) {
((TestElement) testElement).setStatus(status);
}
/**
* Returns an array of all failed {@link ITestElement}s
*
* @return an array of failed {@link ITestElement}s
*/
public List<TestElement> getAllFailedTestElements() {
List<TestElement> failures = new ArrayList<>();
addFailures(failures, this);
return Collections.unmodifiableList(failures);
}
private void addFailures(Collection<TestElement> failures, TestElement testElement) {
Result testResult = testElement.getTestResult(true);
if (testResult == Result.ERROR || testResult == Result.FAILURE) {
failures.add(testElement);
}
if (testElement instanceof TestSuiteElement) {
TestSuiteElement testSuiteElement = (TestSuiteElement) testElement;
for (TestElement child : testSuiteElement.getChildren()) {
addFailures(failures, child);
}
}
}
@Override
public String toString() {
return fTestRunName + " " + DateFormat.getDateTimeInstance().format(new Date(fStartTime.toEpochMilli())); //$NON-NLS-1$
}
@Override
public TestSuiteElement getParent() {
return null;
}
@Override
public String getTestName() {
return getTestRunName();
}
/**
* An abstract base class for a listener safe runnable
*/
public abstract class ListenerSafeRunnable implements ISafeRunnable {
@Override
public void handleException(Throwable exception) {
UnitTestPlugin.log(exception);
}
}
@Override
public TestCaseElement newTestCase(String testId, String testName, ITestSuiteElement parent, String displayName,
String data) {
return (TestCaseElement) fSessionNotifier.testTreeEntry(testId, testName, false, Integer.valueOf(1), false,
parent, displayName, data);
}
@Override
public TestSuiteElement newTestSuite(String testId, String testName, Integer testCount, ITestSuiteElement parent,
String displayName, String data) {
return (TestSuiteElement) fSessionNotifier.testTreeEntry(testId, testName, true, testCount, testCount == null,
parent, displayName, data);
}
@Override
public void notifyTestSessionAborted(final Duration reportDuration, Exception cause) {
if (isStopped()) {
return;
}
if (reportDuration != null) {
setDuration(reportDuration);
}
fTestRunnerClient.stopMonitoring();
this.completedOrAborted = true;
SafeRunner.run(new ListenerSafeRunnable() {
@Override
public void run() {
fSessionNotifier.testRunStopped(fDuration);
}
});
}
@Override
public void notifyTestSessionCompleted(final Duration reportDuration) {
if (isStopped()) {
return;
}
if (reportDuration != null) {
setDuration(reportDuration);
}
fTestRunnerClient.stopMonitoring();
this.completedOrAborted = true;
SafeRunner.run(new ListenerSafeRunnable() {
@Override
public void run() {
fSessionNotifier.testRunEnded(fDuration);
}
});
}
@Override
public void notifyTestEnded(ITestElement test, boolean isIgnored) {
if (isStopped()) {
return;
}
SafeRunner.run(new ListenerSafeRunnable() {
@Override
public void run() {
fSessionNotifier.testEnded(test, isIgnored);
}
});
}
@Override
public void notifyTestStarted(ITestElement test) {
if (isStopped()) {
return;
}
SafeRunner.run(new ListenerSafeRunnable() {
@Override
public void run() {
fSessionNotifier.testStarted(test);
}
});
}
@Override
public void notifyTestSessionStarted(final Integer count) {
if (isStopped()) {
return;
}
this.predefinedTestCount = count;
SafeRunner.run(new ListenerSafeRunnable() {
@Override
public void run() {
fSessionNotifier.testRunStarted(count);
}
});
}
@Override
public void notifyTestFailed(ITestElement test, Result status, boolean isAssumptionFailed,
FailureTrace failureTrace) {
if (isStopped()) {
return;
}
if (status != Result.FAILURE && status != Result.ERROR) {
throw new IllegalArgumentException("Status has to be FAILURE or ERROR"); //$NON-NLS-1$
}
SafeRunner.run(new ListenerSafeRunnable() {
@Override
public void run() {
fSessionNotifier.testFailed(test, status, isAssumptionFailed, failureTrace);
}
});
}
@Override
public Integer getFinalTestCaseCount() {
if (predefinedTestCount != null) {
return predefinedTestCount;
}
if (getChildren().isEmpty()) {
return null;
}
if (!isRunning()) {
int res = 0;
for (TestElement child : getChildren()) {
Integer childCount = child.getFinalTestCaseCount();
if (childCount == null) {
return null;
}
res += childCount.intValue();
}
return Integer.valueOf(res);
}
return null;
}
@Override
public Result getTestResult(boolean includeChildren) {
return this.fTestResult != null ? this.fTestResult : super.getTestResult(includeChildren);
}
}