/******************************************************************************* * Copyright (c) 2011, 2013 Anton Gorenkov 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: * Anton Gorenkov - initial API and implementation * Marc-Andre Laperle (Ericsson) *******************************************************************************/ package org.eclipse.cdt.testsrunner.internal.model; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Stack; import org.eclipse.cdt.testsrunner.model.IModelVisitor; import org.eclipse.cdt.testsrunner.model.ITestMessage; import org.eclipse.cdt.testsrunner.model.ITestModelAccessor; import org.eclipse.cdt.testsrunner.model.ITestModelUpdater; import org.eclipse.cdt.testsrunner.model.ITestingSessionListener; import org.eclipse.cdt.testsrunner.model.ITestCase; import org.eclipse.cdt.testsrunner.model.ITestItem; import org.eclipse.cdt.testsrunner.model.ITestItem.Status; import org.eclipse.cdt.testsrunner.model.ITestMessage.Level; import org.eclipse.cdt.testsrunner.model.ITestSuite; /** * Manages the testing model (creates, fill and update it) and notifies the * listeners about updates. */ public class TestModelManager implements ITestModelUpdater, ITestModelAccessor { /** * Name of the root test suite. * * @note Root test suite is invisible (only its children are visible), so * the name is not important. */ public static final String ROOT_TEST_SUITE_NAME = "<root>"; //$NON-NLS-1$ /** Stack of the currently entered (and not existed) test suites. */ private Stack<TestSuite> testSuitesStack = new Stack<TestSuite>(); /** * Currently running test case. There are no nested test cases, so the * collection is not necessary. */ private TestCase currentTestCase = null; /** * The mapping of test suite object to the index on which it was inserted to * the parent. * * @note Test suites presence in this map means that test suite was visited * during the testing process (not visited test suites are removed when * testing is finished cause they are considered as renamed or removed). * @note Test suite insert position is important for insertion algorithm. */ private Map<TestItem, Integer> testSuitesIndex = new HashMap<TestItem, Integer>(); /** Listeners collection. */ private List<ITestingSessionListener> listeners = new ArrayList<ITestingSessionListener>(); /** Flag stores whether test execution time should be measured for the session. */ private boolean timeMeasurement = false; /** Stores the test case start time or 0 there is no currently running test case. */ private long testCaseStartTime = 0; /** Instance of the insertion algorithm for test suites. */ private TestSuiteInserter testSuiteInserter = new TestSuiteInserter(); /** Instance of the insertion algorithm for test cases. */ private TestCaseInserter testCaseInserter = new TestCaseInserter(); /** * Builds current tests hierarchy from the other one (copies only necessary * information). */ private class HierarchyCopier implements IModelVisitor { @Override public void visit(ITestSuite testSuite) { // Do not copy root test suite if (testSuite.getParent() != null) { enterTestSuite(testSuite.getName()); } } @Override public void leave(ITestSuite testSuite) { // Do not copy root test suite if (testSuite.getParent() != null) { exitTestSuite(); } } @Override public void visit(ITestCase testCase) { enterTestCase(testCase.getName()); setTestStatus(TestCase.Status.NotRun); } @Override public void leave(ITestCase testCase) { exitTestCase(); } @Override public void visit(ITestMessage testMessage) {} @Override public void leave(ITestMessage testMessage) {} } /** * Utility class: generalization of insertion algorithm for test suites and * test cases. * * <p> * The algorithm tries to find the place where the new item should be * inserted at. If the item with such name does not exist in the current top * most test suite, it should be inserted at the current position. If it * already exists (at the next or previous position) then it should be moved * from there to the current one. * </p> * * @param <E> test item type (test suite or test case) */ private abstract class TestItemInserter<E extends TestItem> { /** * Check whether item has the required type (test suite for suites inserter and * test case for cases one). * * @param item test item to check * @return whether item has the required type */ protected abstract boolean isRequiredTestItemType(TestItem item); /** * Creates a new item type with the specified name and parent (test * suite for suites inserter and test case for cases one). * * @param name name of the new test item * @param parent parent for the new test item * @return new test item */ protected abstract E createTestItem(String name, TestSuite parent); /** * Save new test item in the tracking structures (suite in stack, case * in current variable). Additional operations (e.g. listeners * notification about item entering) can be done too. * * @param item new test item */ protected abstract void addNewTestItem(E item); /** * Returns the casted test item if it matches by name and type or * <code>null</code> if it doesn't. * * @param item test item to check * @param name test item name * @return casted test item or null */ @SuppressWarnings("unchecked") private E checkTestItem(TestItem item, String name) { return (isRequiredTestItemType(item) && item.getName().equals(name)) ? (E)item : null; } /** * Returns the last insert index for the specified test suite. Returns 0 * if test suite was not inserted yet. * * @param testSuite test suite to look up * @return insert index or 0 */ private int getLastInsertIndex(TestSuite testSuite) { Integer intLastInsertIndex = testSuitesIndex.get(testSuite); return intLastInsertIndex != null ? intLastInsertIndex : 0; } /** * Notifies the listeners about children update of the specified test * suite. * * @param suite updated test suite */ private void notifyAboutChildrenUpdate(ITestSuite suite) { for (ITestingSessionListener listener : getListenersCopy()) { listener.childrenUpdate(suite); } } /** * Inserts the test item by the name. * * @param name test item name */ public void insert(String name) { TestSuite currTestSuite = testSuitesStack.peek(); int lastInsertIndex = getLastInsertIndex(currTestSuite); List<TestItem> children = currTestSuite.getChildrenList(); E newTestItem = null; // Optimization: Check whether we already pointing to the test suite with required name try { newTestItem = checkTestItem(children.get(lastInsertIndex), name); } catch (IndexOutOfBoundsException e) {} if (newTestItem != null) { testSuitesIndex.put(currTestSuite, lastInsertIndex+1); } // Check whether the suite with required name was later in the hierarchy if (newTestItem == null) { for (int childIndex = lastInsertIndex; childIndex < children.size(); childIndex++) { newTestItem = checkTestItem(children.get(childIndex), name); if (newTestItem != null) { testSuitesIndex.put(currTestSuite, childIndex); break; } } } // Search in previous if (newTestItem == null) { for (int childIndex = 0; childIndex < lastInsertIndex; childIndex++) { newTestItem = checkTestItem(children.get(childIndex), name); if (newTestItem != null) { TestItem removed = children.remove(childIndex); lastInsertIndex = Math.min(lastInsertIndex, children.size()); children.add(lastInsertIndex, removed); notifyAboutChildrenUpdate(currTestSuite); break; } } } // Add new if (newTestItem == null) { newTestItem = createTestItem(name, currTestSuite); children.add(lastInsertIndex, newTestItem); testSuitesIndex.put(currTestSuite, lastInsertIndex+1); notifyAboutChildrenUpdate(currTestSuite); } if (!testSuitesIndex.containsKey(newTestItem)) { testSuitesIndex.put(newTestItem, 0); } addNewTestItem(newTestItem); } } /** * Utility class: insertion algorithm specialization for test suites. */ private class TestSuiteInserter extends TestItemInserter<TestSuite> { @Override protected boolean isRequiredTestItemType(TestItem item) { return (item instanceof TestSuite); } @Override protected TestSuite createTestItem(String name, TestSuite parent) { return new TestSuite(name, parent); } @Override protected void addNewTestItem(TestSuite testSuite) { testSuitesStack.push(testSuite); // Notify listeners for (ITestingSessionListener listener : getListenersCopy()) { listener.enterTestSuite(testSuite); } } } /** * Utility class: insertion algorithm specialization for test cases. */ private class TestCaseInserter extends TestItemInserter<TestCase> { @Override protected boolean isRequiredTestItemType(TestItem item) { return (item instanceof TestCase); } @Override protected TestCase createTestItem(String name, TestSuite parent) { return new TestCase(name, parent); } @Override protected void addNewTestItem(TestCase testCase) { currentTestCase = testCase; testCase.setStatus(ITestItem.Status.Skipped); // Notify listeners for (ITestingSessionListener listener : getListenersCopy()) { listener.enterTestCase(testCase); } } } public TestModelManager(ITestSuite previousTestsHierarchy, boolean timeMeasurement) { testSuitesStack.push(new TestSuite(ROOT_TEST_SUITE_NAME, null)); if (previousTestsHierarchy != null) { // Copy tests hierarchy this.timeMeasurement = false; previousTestsHierarchy.visit(new HierarchyCopier()); } this.timeMeasurement = timeMeasurement; this.testSuitesIndex.clear(); } /** * Notifies the listeners that testing was started. */ public void testingStarted() { // Notify listeners for (ITestingSessionListener listener : getListenersCopy()) { listener.testingStarted(); } } /** * Removes not visited test items and notifies the listeners that testing * was finished. */ public void testingFinished() { // Remove all NotRun-tests and not used test suites (probably they were removed from test module) getRootSuite().visit(new IModelVisitor() { @Override public void visit(ITestSuite testSuite) { List<TestItem> suiteChildren = ((TestSuite)testSuite).getChildrenList(); for (Iterator<TestItem> it = suiteChildren.iterator(); it.hasNext();) { TestItem item = it.next(); if ((item instanceof ITestSuite && !testSuitesIndex.containsKey(item)) || (item instanceof ITestCase && item.getStatus() == ITestItem.Status.NotRun)) { it.remove(); } } } @Override public void visit(ITestMessage testMessage) {} @Override public void visit(ITestCase testCase) {} @Override public void leave(ITestSuite testSuite) {} @Override public void leave(ITestCase testCase) {} @Override public void leave(ITestMessage testMessage) {} }); testSuitesIndex.clear(); // Notify listeners for (ITestingSessionListener listener : getListenersCopy()) { listener.testingFinished(); } } @Override public void enterTestSuite(String name) { testSuiteInserter.insert(name); } @Override public void exitTestSuite() { exitTestCase(); TestSuite testSuite = testSuitesStack.pop(); // Notify listeners for (ITestingSessionListener listener : getListenersCopy()) { listener.exitTestSuite(testSuite); } } @Override public void enterTestCase(String name) { testCaseInserter.insert(name); if (timeMeasurement) { testCaseStartTime = System.currentTimeMillis(); } } @Override public void setTestStatus(Status status) { currentTestCase.setStatus(status); } @Override public void setTestingTime(int testingTime) { currentTestCase.setTestingTime(testingTime); } @Override public void exitTestCase() { if (currentTestCase != null) { // Set test execution time (if time measurement is turned on) if (timeMeasurement) { int testingTime = (int)(System.currentTimeMillis()-testCaseStartTime); currentTestCase.setTestingTime(currentTestCase.getTestingTime()+testingTime); testCaseStartTime = 0; } TestCase testCase = currentTestCase; currentTestCase = null; // Notify listeners for (ITestingSessionListener listener : getListenersCopy()) { listener.exitTestCase(testCase); } } } @Override public void addTestMessage(String file, int line, Level level, String text) { TestLocation location = (file == null || file.isEmpty() || line <= 0) ? null : new TestLocation(file, line); currentTestCase.addTestMessage(new TestMessage(location, level, text)); } @Override public ITestSuite currentTestSuite() { return testSuitesStack.peek(); } @Override public ITestCase currentTestCase() { return currentTestCase; } @Override public boolean isCurrentlyRunning(ITestItem item) { return (item == currentTestCase && item != null) || testSuitesStack.contains(item); } @Override public TestSuite getRootSuite() { return testSuitesStack.firstElement(); } @Override public void addChangesListener(ITestingSessionListener listener) { synchronized (listeners) { listeners.add(listener); } } @Override public void removeChangesListener(ITestingSessionListener listener) { synchronized (listeners) { listeners.remove(listener); } } /** * Copies listeners before notifying them to avoid dead-locks. * * @return listeners collection copy */ private ITestingSessionListener[] getListenersCopy() { synchronized (listeners) { return listeners.toArray(new ITestingSessionListener[listeners.size()]); } } }