/******************************************************************************* * Copyright (c) 2000, 2009 IBM Corporation 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.phpsrc.eclipse.pti.tools.phpunit.core.model; import java.io.File; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.ListenerList; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchManager; import org.phpsrc.eclipse.pti.core.launching.PHPToolLauncher; import org.phpsrc.eclipse.pti.tools.phpunit.PHPUnitPlugin; import org.phpsrc.eclipse.pti.tools.phpunit.core.MessageIds; import org.phpsrc.eclipse.pti.tools.phpunit.core.launcher.ITestKind; import org.phpsrc.eclipse.pti.tools.phpunit.core.model.TestElement.Status; import org.phpsrc.eclipse.pti.ui.Logger; /** * A test run session holds all information about a test run, i.e. launch * configuration, launch, test tree (including results). */ public class TestRunSession implements ITestRunSession { /** * The launch, or <code>null</code> iff this session was run externally. */ private final ILaunch fLaunch; private final String fTestRunName; /** * Java project, or <code>null</code>. */ private final IProject fProject; private final IFile fTestFile; private final ITestKind fTestRunnerKind; /** * Test runner client or <code>null</code>. */ private JsonTestRunnerClient fTestRunnerClient; private final ListenerList/* <ITestSessionListener> */fSessionListeners; /** * The model root, or <code>null</code> if swapped to disk. */ private TestRoot fTestRoot; /** * 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; /** * The TestSuites for which additional children are expected. */ private List/* <IncompleteTestSuite> */fIncompleteTestSuites; /** * Suite for unrooted test case elements, or <code>null</code>. */ private TestSuiteElement fUnrootedSuite; /** * Number of tests started during this test run. */ volatile int fStartedCount; /** * Number of tests ignored during this test run. */ volatile int fIgnoredCount; /** * Number of errors during this test run. */ volatile int fErrorCount; /** * Number of failures during this test run. */ volatile int fFailureCount; /** * Total number of tests to run. */ volatile int fTotalCount; /** * Start time in millis. */ volatile long fStartTime; volatile boolean fIsRunning; volatile boolean fIsStopped; public TestRunSession(String testRunName, IFile testFile) { this(testRunName, testFile != null ? testFile.getProject() : null, testFile); } public TestRunSession(String testRunName, IProject project) { this(testRunName, project, null); } public TestRunSession(PHPToolLauncher launcher, String testRunName, IFile testFile) { this(testRunName, testFile != null ? testFile.getProject() : null, testFile); fTestRunnerClient = new JsonTestRunnerClient(); fTestRunnerClient.startListening(new ITestRunListener[] { new TestSessionNotifier() }); addTestSessionListener(new TestRunListenerAdapter(this)); } public TestRunSession(PHPToolLauncher launcher, String testRunName, IProject project) { this(testRunName, project, null); fTestRunnerClient = new JsonTestRunnerClient(); fTestRunnerClient.startListening(new ITestRunListener[] { new TestSessionNotifier() }); addTestSessionListener(new TestRunListenerAdapter(this)); } private TestRunSession(String testRunName, IProject project, IFile testFile) { // TODO: check assumptions about non-null fields Assert.isNotNull(testRunName); fLaunch = null; fProject = project; fTestFile = testFile; fTestRunName = testRunName; fTestRunnerKind = ITestKind.NULL; // TODO fTestRoot = new TestRoot(this); fIdToTest = new HashMap(); fTestRunnerClient = null; fSessionListeners = new ListenerList(); } void reset() { fStartedCount = 0; fFailureCount = 0; fErrorCount = 0; fIgnoredCount = 0; fTotalCount = 0; fTestRoot = new TestRoot(this); fTestResult = null; fIdToTest = new HashMap(); } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.ITestRunSession#getProgressState() */ public ProgressState getProgressState() { if (isRunning()) { return ProgressState.RUNNING; } if (isStopped()) { return ProgressState.STOPPED; } return ProgressState.COMPLETED; } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.model.ITestElement#getTestResult(boolean) */ public Result getTestResult(boolean includeChildren) { if (fTestRoot != null) { return fTestRoot.getTestResult(true); } else { return fTestResult; } } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.model.ITestElementContainer#getChildren() */ public ITestElement[] getChildren() { return getTestRoot().getChildren(); } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.model.ITestElement#getFailureTrace() */ public FailureTrace getFailureTrace() { return null; } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.model.ITestElement#getParentContainer() */ public ITestElementContainer getParentContainer() { return null; } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.model.ITestElement#getTestRunSession() */ public ITestRunSession getTestRunSession() { return this; } public TestRoot getTestRoot() { swapIn(); // TODO: TestRoot should stay (e.g. for // getTestRoot().getStatus()) return fTestRoot; } /** * @return the Java project, or <code>null</code> */ public IProject getLaunchedProject() { return fProject; } public ITestKind getTestRunnerKind() { return fTestRunnerKind; } /** * @return the launch, or <code>null</code> iff this session was run * externally */ public ILaunch getLaunch() { return fLaunch; } public String getTestRunName() { return fTestRunName; } public int getErrorCount() { return fErrorCount; } public int getFailureCount() { return fFailureCount; } public int getStartedCount() { return fStartedCount; } public int getIgnoredCount() { return fIgnoredCount; } public int getTotalCount() { return fTotalCount; } public long getStartTime() { return fStartTime; } public IFile getTestFile() { return fTestFile; } /** * @return <code>true</code> if the session has been stopped or terminated */ public boolean isStopped() { return fIsStopped; } public void addTestSessionListener(ITestSessionListener listener) { swapIn(); fSessionListeners.add(listener); } public void removeTestSessionListener(ITestSessionListener listener) { fSessionListeners.remove(listener); } public void swapOut() { if (fTestRoot == null) return; if (isRunning() || isStarting() || isKeptAlive()) return; Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ITestSessionListener registered = (ITestSessionListener) listeners[i]; if (!registered.acceptsSwapToDisk()) return; } try { File swapFile = getSwapFile(); PHPUnitModel.exportTestRunSession(this, swapFile); fTestResult = fTestRoot.getTestResult(true); fTestRoot = null; fTestRunnerClient = null; fIdToTest = new HashMap(); fIncompleteTestSuites = null; fUnrootedSuite = null; } catch (IllegalStateException e) { Logger.logException(e); } catch (CoreException e) { Logger.logException(e); } } public boolean isStarting() { return getStartTime() == 0 && fLaunch != null && !fLaunch.isTerminated(); } public void removeSwapFile() { File swapFile = getSwapFile(); if (swapFile.exists()) swapFile.delete(); } private File getSwapFile() throws IllegalStateException { File historyDir = PHPUnitPlugin.getHistoryDirectory(); String isoTime = new SimpleDateFormat("yyyyMMdd-HHmmss.SSS").format(new Date(getStartTime())); //$NON-NLS-1$ String swapFileName = isoTime + ".xml"; //$NON-NLS-1$ return new File(historyDir, swapFileName); } public void swapIn() { if (fTestRoot != null) return; try { PHPUnitModel.importIntoTestRunSession(getSwapFile(), this); } catch (IllegalStateException e) { Logger.logException(e); fTestRoot = new TestRoot(this); fTestResult = null; } catch (CoreException e) { Logger.logException(e); fTestRoot = new TestRoot(this); fTestResult = null; } } public void stopTestRun() { if (isRunning() || !isKeptAlive()) fIsStopped = true; if (fTestRunnerClient != null) fTestRunnerClient.stopTest(); } /** * @return <code>true</code> iff the runtime VM of this test session is * still alive */ public boolean isKeptAlive() { return false; // if (fTestRunnerClient != null && fLaunch != null && // fTestRunnerClient.isRunning() // && ILaunchManager.DEBUG_MODE.equals(fLaunch.getLaunchMode())) { // ILaunchConfiguration config = fLaunch.getLaunchConfiguration(); // try { // return config != null && // config.getAttribute(JUnitLaunchConfigurationConstants.ATTR_KEEPRUNNING, // false); // } catch (CoreException e) { // return false; // } // // } else { // return false; // } } /** * @return <code>true</code> iff this session has been started, but not * ended nor stopped nor terminated */ public boolean isRunning() { return fIsRunning; } /** * 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} * @return <code>false</code> iff the rerun could not be started * @throws CoreException * if the launch fails */ public boolean rerunTest(String testId, String className, String testName, String launchMode) throws CoreException { // if (isKeptAlive()) { // Status status = ((TestCaseElement) // getTestElement(testId)).getStatus(); // if (status == Status.ERROR) { // fErrorCount--; // } else if (status == Status.FAILURE) { // fFailureCount--; // } // fTestRunnerClient.rerunTest(testId, className, testName); // return true; // // } else if (fLaunch != null) { // // run the selected test using the previous launch configuration // ILaunchConfiguration launchConfiguration = // fLaunch.getLaunchConfiguration(); // if (launchConfiguration != null) { // // String name = className; // if (testName != null) // name += "." + testName; //$NON-NLS-1$ // String configName = // Messages.format(JUnitMessages.TestRunnerViewPart_configName, name); // 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(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, ""); //$NON-NLS-1$ // if (testName != null) { // tmp.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, // testName); // // String args= "-rerun "+testId; // // // tmp.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, // // args); // } // tmp.launch(launchMode, null); // return true; // } // } return false; } public TestElement getTestElement(String id) { return (TestElement) fIdToTest.get(id); } private TestElement addTreeEntry(String treeEntry) { // format: testId<,>testName<,>isSuite<,>testcount String[] params=treeEntry.split(JsonTestRunnerClient.PARAM_SEP); int i=0; String id = params[i++].trim(); String testName = params[i++].trim(); boolean isSuite = params[i++].trim().equals("true"); //$NON-NLS-1$ int testCount = Integer.parseInt(params[i++].trim()); if (fIncompleteTestSuites.isEmpty()) { return createTestElement(fTestRoot, id, testName, isSuite, testCount); } else { int suiteIndex = fIncompleteTestSuites.size() - 1; IncompleteTestSuite openSuite = (IncompleteTestSuite) fIncompleteTestSuites.get(suiteIndex); openSuite.fOutstandingChildren--; if (openSuite.fOutstandingChildren <= 0) fIncompleteTestSuites.remove(suiteIndex); return createTestElement(openSuite.fTestSuiteElement, id, testName, isSuite, testCount); } } public TestElement createTestElement(TestSuiteElement parent, String id, String testName, boolean isSuite, int testCount) { TestElement testElement; if (isSuite) { TestSuiteElement testSuiteElement = new TestSuiteElement(parent, id, testName, testCount); testElement = testSuiteElement; if (testCount > 0) fIncompleteTestSuites.add(new IncompleteTestSuite(testSuiteElement, testCount)); } else { testElement = new TestCaseElement(parent, id, testName); } fIdToTest.put(id, testElement); return testElement; } /** * Append the test name from <code>s</code> to <code>testName</code>. * * @param s * the string to scan * @param start * the offset of the first character in <code>s</code> * @param testName * the result * * @return the index of the next ',' */ private int scanTestName(String s, int start, StringBuffer testName) { boolean inQuote = false; int i = start; for (; i < s.length(); i++) { char c = s.charAt(i); if (c == '\\' && !inQuote) { inQuote = true; continue; } else if (inQuote) { inQuote = false; testName.append(c); } else if (c == ',') break; else testName.append(c); } return i; } /** * An {@link ITestRunListener} that listens to events from the * {@link RemoteTestRunnerClient} and translates them into high-level model * events (broadcasted to {@link ITestSessionListener}s). */ private class TestSessionNotifier implements ITestRunListener { public void testRunStarted(int testCount) { fIncompleteTestSuites = new ArrayList(); fStartedCount = 0; fIgnoredCount = 0; fFailureCount = 0; fErrorCount = 0; fTotalCount = testCount; fStartTime = System.currentTimeMillis(); fIsRunning = true; Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).sessionStarted(); } } public void testRunEnded(long elapsedTime) { fIsRunning = false; Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).sessionEnded(elapsedTime); } } public void testRunStopped(long elapsedTime) { fIsRunning = false; fIsStopped = true; Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).sessionStopped(elapsedTime); } } public void testRunTerminated() { fIsRunning = false; fIsStopped = true; Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).sessionTerminated(); } } /* * (non-Javadoc) * * @see * org.eclipse.jdt.internal.junit.model.ITestRunListener2#testTreeEntry * (java.lang.String) */ public void testTreeEntry(String description) { TestElement testElement = addTreeEntry(description); Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).testAdded(testElement); } } private TestElement createUnrootedTestElement(String testId, String testName) { TestSuiteElement unrootedSuite = getUnrootedSuite(); TestElement testElement = createTestElement(unrootedSuite, testId, testName, false, 1); Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).testAdded(testElement); } return testElement; } private TestSuiteElement getUnrootedSuite() { if (fUnrootedSuite == null) { fUnrootedSuite = (TestSuiteElement) createTestElement(fTestRoot, "-2", "Unrooted tests", true, 0); } return fUnrootedSuite; } public void testStarted(String testId, String testName) { if (fStartedCount == 0) { Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).runningBegins(); } } TestElement testElement = getTestElement(testId); if (testElement == null) { testElement = createUnrootedTestElement(testId, testName); } else if (!(testElement instanceof TestCaseElement)) { logUnexpectedTest(testId, testElement); return; } TestCaseElement testCaseElement = (TestCaseElement) testElement; setStatus(testCaseElement, Status.RUNNING); fStartedCount++; Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).testStarted(testCaseElement); } } public void testEnded(String testId, String testName) { TestElement testElement = getTestElement(testId); if (testElement == null) { testElement = createUnrootedTestElement(testId, testName); } else if (!(testElement instanceof TestCaseElement)) { logUnexpectedTest(testId, testElement); return; } TestCaseElement testCaseElement = (TestCaseElement) testElement; if (testName.startsWith(MessageIds.IGNORED_TEST_PREFIX)) { testCaseElement.setIgnored(true); fIgnoredCount++; } if (testCaseElement.getStatus() == Status.RUNNING) setStatus(testCaseElement, Status.OK); Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) 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) */ public void testFailed(int statusCode, String testId, String testName, String trace, String expected, String actual) { TestElement testElement = getTestElement(testId); if (testElement == null) { testElement = createUnrootedTestElement(testId, testName); return; } Status status = Status.convert(statusCode); registerTestFailureStatus(testElement, status, trace, nullifyEmpty(expected), nullifyEmpty(actual)); Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { ((ITestSessionListener) listeners[i]).testFailed(testElement, status, trace, expected, actual); } } private String nullifyEmpty(String string) { if (string == null) return null; int length = string.length(); if (length == 0) return null; else if (string.charAt(length - 1) == '\n') return string.substring(0, length - 1); else return string; } /* * (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) */ public void testReran(String testId, String className, String testName, int statusCode, String trace, String expectedResult, String actualResult) { TestElement testElement = getTestElement(testId); if (testElement == null) { testElement = createUnrootedTestElement(testId, testName); } else if (!(testElement instanceof TestCaseElement)) { logUnexpectedTest(testId, testElement); return; } TestCaseElement testCaseElement = (TestCaseElement) testElement; Status status = Status.convert(statusCode); registerTestFailureStatus(testElement, status, trace, nullifyEmpty(expectedResult), nullifyEmpty(actualResult)); Object[] listeners = fSessionListeners.getListeners(); for (int i = 0; i < listeners.length; ++i) { // TODO: post old & new status? ((ITestSessionListener) listeners[i]).testReran(testCaseElement, status, trace, expectedResult, actualResult); } } private void logUnexpectedTest(String testId, TestElement testElement) { Logger .logException(new Exception( "Unexpected TestElement type for testId '" + testId + "': " + testElement)); //$NON-NLS-1$ //$NON-NLS-2$ } } private static class IncompleteTestSuite { public TestSuiteElement fTestSuiteElement; public int fOutstandingChildren; public IncompleteTestSuite(TestSuiteElement testSuiteElement, int outstandingChildren) { fTestSuiteElement = testSuiteElement; fOutstandingChildren = outstandingChildren; } } public void registerTestFailureStatus(TestElement testElement, Status status, String trace, String expected, String actual) { testElement.setStatus(status, trace, expected, actual); if (status.isError()) { fErrorCount++; } else if (status.isFailure()) { fFailureCount++; } } public void registerTestEnded(TestElement testElement, boolean completed) { if (testElement instanceof TestCaseElement) { fTotalCount++; if (!completed) { return; } fStartedCount++; if (((TestCaseElement) testElement).isIgnored()) { fIgnoredCount++; } if (!testElement.getStatus().isErrorOrFailure()) setStatus(testElement, Status.OK); } } private void setStatus(TestElement testElement, Status status) { testElement.setStatus(status); } public TestElement[] getAllFailedTestElements() { ArrayList failures = new ArrayList(); addFailures(failures, getTestRoot()); return (TestElement[]) failures.toArray(new TestElement[failures.size()]); } private void addFailures(ArrayList failures, ITestElement testElement) { Result testResult = testElement.getTestResult(true); if (testResult == Result.ERROR || testResult == Result.FAILURE) { failures.add(testElement); } if (testElement instanceof TestSuiteElement) { TestSuiteElement testSuiteElement = (TestSuiteElement) testElement; ITestElement[] children = testSuiteElement.getChildren(); for (int i = 0; i < children.length; i++) { addFailures(failures, children[i]); } } } /* * (non-Javadoc) * * @see org.eclipse.jdt.junit.model.ITestElement#getElapsedTimeInSeconds() */ public double getElapsedTimeInSeconds() { if (fTestRoot == null) return Double.NaN; return fTestRoot.getElapsedTimeInSeconds(); } }