| /******************************************************************************* |
| * Copyright (c) 2011 Anton Gorenkov |
| * 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 |
| *******************************************************************************/ |
| package org.eclipse.cdt.testsrunner.internal.gtest; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.text.MessageFormat; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.cdt.testsrunner.model.ITestModelUpdater; |
| import org.eclipse.cdt.testsrunner.model.ITestItem; |
| import org.eclipse.cdt.testsrunner.model.ITestMessage; |
| import org.eclipse.cdt.testsrunner.model.TestingException; |
| import org.xml.sax.SAXException; |
| |
| |
| /** |
| * <p> |
| * Parses the output of Google Testing Framework and notifies the Tests Runner |
| * Core about how the testing process is going. |
| * </p> |
| * <p> |
| * Unfortunately, gtest does not provide a realtime XML output (yes, it has XML |
| * output, but it is generated after testing process is done), so we have to |
| * parse its output that is less reliable. |
| * </p> |
| * <p> |
| * The parsing is done with a simple FSM (Final State Machine). There is an |
| * internal state that changes when input tokens (gtest output lines) come. |
| * There is a transitions table that is used to determine what is the next state |
| * depending on the current one and the input token. The state may define |
| * onEnter and onExit actions to do the useful job. |
| * </p> |
| */ |
| public class OutputHandler { |
| |
| /** |
| * Base class for the FSM internal state. |
| */ |
| class State { |
| |
| /** Stores the regular expression by which the state should be entered. */ |
| private Pattern enterPattern; |
| |
| /** The regular expression matcher. */ |
| private Matcher matcher; |
| |
| /** Groups count in a regular expression. */ |
| private int groupCount; |
| |
| /** |
| * The constructor. |
| * |
| * @param enterRegex the regular expression by which the state should be |
| * entered |
| */ |
| State(String enterRegex) { |
| this(enterRegex, -1); |
| } |
| |
| /** |
| * The constructor. |
| * |
| * @param enterRegex the regular expression by which the state should be |
| * entered |
| * @param groupCount groups count in a regular expression. It is used |
| * just to make debug easier and the parser more reliable. |
| */ |
| State(String enterRegex, int groupCount) { |
| enterPattern = Pattern.compile(enterRegex); |
| this.groupCount = groupCount; |
| } |
| |
| /** |
| * Checks whether the specified string matches the enter pattern |
| * (regular expression). If it is so the state should be entered. |
| * |
| * @param line input line (token) |
| * @return true if matches and false otherwise |
| * @throws TestingException if groups count does not match the defined |
| * in constructor number. |
| */ |
| public boolean match(String line) throws TestingException { |
| matcher = enterPattern.matcher(line); |
| boolean groupsCountOk = groupCount == -1 || matcher.groupCount() == groupCount; |
| if (!groupsCountOk) { |
| generateInternalError( |
| MessageFormat.format( |
| GoogleTestsRunnerMessages.OutputHandler_wrong_groups_count, |
| enterPattern.pattern(), matcher.groupCount(), groupCount |
| ) |
| ); |
| } |
| boolean matches = matcher.matches(); |
| if (!matches || !groupsCountOk) { |
| // Do not keep the reference - it will be unnecessary anyway |
| matcher = null; |
| } |
| return matches; |
| } |
| |
| /** |
| * Returns the matched group value by index. |
| * |
| * @param groupNumber group index |
| * @return group value |
| */ |
| protected String group(int groupNumber) { |
| return matcher.group(groupNumber); |
| } |
| |
| /** |
| * Action that triggers on state enter. |
| * |
| * @param previousState previous state |
| * @throws TestingException if testing error is detected |
| */ |
| public void onEnter(State previousState) throws TestingException {} |
| |
| /** |
| * Action that triggers on state exit. |
| * |
| * @param previousState next state |
| * @throws TestingException if testing error is detected |
| */ |
| public void onExit(State nextState) {} |
| |
| /** |
| * Common routine that constructs full test suite name by name and type |
| * parameter. |
| * |
| * @param name test suite name |
| * @param typeParameter type parameter |
| * @return full test suite name |
| */ |
| protected String getTestSuiteName(String name, String typeParameter) { |
| return (typeParameter != null) ? MessageFormat.format("{0}({1})", name, typeParameter.trim()) : name; //$NON-NLS-1$ |
| } |
| } |
| |
| |
| /** |
| * The state is activated when a new test suite is started. |
| */ |
| class TestSuiteStart extends State { |
| |
| /** Stores the matched type parameter. */ |
| private String typeParameter; |
| |
| TestSuiteStart(String enterRegex, int groupCount) { |
| super(enterRegex, groupCount); |
| } |
| |
| /** |
| * Stores type parameter and notify Tests Runner Core about test suite |
| * start. |
| */ |
| @Override |
| public void onEnter(State previousState) { |
| typeParameter = group(3); |
| modelUpdater.enterTestSuite(getTestSuiteName(group(1), typeParameter)); |
| } |
| |
| /** |
| * Provides access to the matched type parameter. |
| * |
| * @return type parameter value |
| */ |
| public String getTypeParameter() { |
| return typeParameter; |
| } |
| } |
| |
| |
| /** |
| * The state is activated when a new test case is started. |
| */ |
| class TestCaseStart extends State { |
| |
| TestCaseStart(String enterRegex, int groupCount) { |
| super(enterRegex, groupCount); |
| } |
| |
| /** |
| * Extract current test case and test suite names and notify Tests |
| * Runner Core about test case start. |
| * |
| * @throws TestingException if extracted test suite name does not match |
| * last entered test suite name. |
| */ |
| @Override |
| public void onEnter(State previousState) throws TestingException { |
| String testCaseName = group(2); |
| String lastTestSuiteName = modelUpdater.currentTestSuite().getName(); |
| String currTestSuiteName = getTestSuiteName(group(1), stateTestSuiteStart.getTypeParameter()); |
| if (!lastTestSuiteName.equals(currTestSuiteName)) { |
| generateInternalError( |
| MessageFormat.format( |
| GoogleTestsRunnerMessages.OutputHandler_wrong_suite_name, |
| testCaseName, currTestSuiteName, lastTestSuiteName |
| ) |
| ); |
| } |
| modelUpdater.enterTestCase(testCaseName); |
| } |
| } |
| |
| |
| /** |
| * The state is activated when an error message's location is started. |
| */ |
| class ErrorMessageLocation extends State { |
| |
| /** Stores the message location file name. */ |
| private String messageFileName; |
| |
| /** Stores the message location line number. */ |
| private int messageLineNumber; |
| |
| /** Stores the first part of the message. */ |
| private String messagePart; |
| |
| ErrorMessageLocation(String enterRegex, int groupCount) { |
| super(enterRegex, groupCount); |
| } |
| |
| /** |
| * Extract the data for the message location (file name, line number). |
| * The data may be provided in a common style ("/path/file:line" with |
| * the message text starting on the next line) or Visual Studio style |
| * ("/path/file(line):" with the message text continuing on the same |
| * line). It is also possible not to specify line number at all |
| * ("/path/file:"). |
| * |
| * @throws TestingException if location format cannot be recognized. |
| */ |
| @Override |
| public void onEnter(State previousState) throws TestingException { |
| String fileNameIfLinePresent = group(2); |
| String fileNameIfLineAbsent = group(6); |
| String lineNumberCommon = group(4); |
| String lineNumberVS = group(5); |
| if (fileNameIfLinePresent != null) { |
| if (lineNumberCommon != null) { |
| messageFileName = fileNameIfLinePresent; |
| messageLineNumber = Integer.parseInt(lineNumberCommon.trim()); |
| } else if (lineNumberVS != null) { |
| messageFileName = fileNameIfLinePresent; |
| messageLineNumber = Integer.parseInt(lineNumberVS.trim()); |
| } else { |
| if (!modelUpdater.currentTestSuite().getName().equals(group(1))) { |
| generateInternalError(GoogleTestsRunnerMessages.OutputHandler_unknown_location_format); |
| } |
| } |
| } else if (fileNameIfLineAbsent != null) { |
| if (lineNumberCommon == null && lineNumberVS == null) { |
| messageFileName = fileNameIfLineAbsent; |
| messageLineNumber = DEFAULT_LOCATION_LINE; |
| } else { |
| generateInternalError(GoogleTestsRunnerMessages.OutputHandler_unknown_location_format); |
| } |
| } |
| // Check special case when file is not known - reset location |
| if (messageFileName.equals("unknown file")) { //$NON-NLS-1$ |
| messageFileName = DEFAULT_LOCATION_FILE; |
| } |
| // NOTE: For Visual Studio style there is also first part of the message at this line |
| messagePart = group(8); |
| } |
| |
| /** |
| * Provides access to the message location file name. |
| * |
| * @return file name |
| */ |
| public String getMessageFileName() { |
| return messageFileName; |
| } |
| |
| /** |
| * Provides access to the message location line number. |
| * |
| * @return line number |
| */ |
| public int getMessageLineNumber() { |
| return messageLineNumber; |
| } |
| |
| /** |
| * Provides access to the first part of the message. |
| * |
| * @return message part |
| */ |
| public String getMessagePart() { |
| return messagePart; |
| } |
| } |
| |
| |
| /** |
| * The state is activated when an error message text is started or continued. |
| */ |
| class ErrorMessage extends State { |
| |
| /** Stores the error message text that was already read. */ |
| private StringBuilder messagePart = new StringBuilder(); |
| |
| ErrorMessage(String enterRegex, int groupCount) { |
| super(enterRegex, groupCount); |
| } |
| |
| /** |
| * Collects the error message parts into internal buffer. If the |
| * previous state is not the same (it should be |
| * stateErrorMessageLocation) - get the message part from it. |
| */ |
| @Override |
| public void onEnter(State previousState) { |
| boolean needEndOfLine = (this == previousState); |
| if (this != previousState) { |
| String firstMessagePart = stateErrorMessageLocation.getMessagePart(); |
| if (firstMessagePart != null) { |
| messagePart.append(firstMessagePart); |
| needEndOfLine = true; |
| } |
| } |
| if (needEndOfLine) { |
| messagePart.append(System.getProperty("line.separator")); //$NON-NLS-1$ |
| } |
| messagePart.append(group(1)); |
| } |
| |
| /** |
| * Notifies the Tests Runner Core about new test message. |
| */ |
| @Override |
| public void onExit(State nextState) { |
| if (this != nextState) { |
| modelUpdater.addTestMessage( |
| stateErrorMessageLocation.getMessageFileName(), |
| stateErrorMessageLocation.getMessageLineNumber(), |
| ITestMessage.Level.Error, |
| messagePart.toString() |
| ); |
| messagePart.setLength(0); |
| } |
| } |
| } |
| |
| |
| /** |
| * The state is activated when a test trace is started or continued. |
| */ |
| class TestTrace extends ErrorMessageLocation { |
| |
| TestTrace(String enterRegex, int groupCount) { |
| super(enterRegex, groupCount); |
| } |
| |
| /** |
| * Notifies the Tests Runner Core about new test message with test trace |
| * info. |
| */ |
| @Override |
| public void onEnter(State previousState) throws TestingException { |
| super.onEnter(previousState); |
| modelUpdater.addTestMessage( |
| getMessageFileName(), |
| getMessageLineNumber(), |
| ITestMessage.Level.Info, |
| getMessagePart() |
| ); |
| } |
| } |
| |
| |
| /** |
| * The state is activated when a test case is finished. |
| */ |
| class TestCaseEnd extends State { |
| |
| TestCaseEnd(String enterRegex, int groupCount) { |
| super(enterRegex, groupCount); |
| } |
| |
| /** |
| * Sets the test case execution time, status and notify Tests Runner |
| * Core about test case end. |
| * |
| * @throws TestingException if current test suite or case name does not |
| * match last entered test suite or case name or if test status is not |
| * known. |
| */ |
| @Override |
| public void onEnter(State previousState) throws TestingException { |
| String lastTestSuiteName = modelUpdater.currentTestSuite().getName(); |
| String explicitTypeParameter = group(5); |
| String typeParameter = explicitTypeParameter != null ? explicitTypeParameter : stateTestSuiteStart.getTypeParameter(); |
| String currTestSuiteName = getTestSuiteName(group(2), typeParameter); |
| if (!lastTestSuiteName.equals(currTestSuiteName)) { |
| generateInternalError( |
| MessageFormat.format( |
| GoogleTestsRunnerMessages.OutputHandler_wrong_suite_name, |
| group(2), currTestSuiteName, lastTestSuiteName |
| ) |
| ); |
| } |
| String lastTestCaseName = modelUpdater.currentTestCase().getName(); |
| if (!lastTestCaseName.equals(group(3))) { |
| generateInternalError( |
| MessageFormat.format( |
| GoogleTestsRunnerMessages.OutputHandler_unexpected_case_end, |
| group(3), lastTestCaseName |
| ) |
| ); |
| } |
| String testStatusStr = group(1); |
| ITestItem.Status testStatus = ITestItem.Status.Skipped; |
| if (testStatusStr.equals(testStatusOk)) { |
| testStatus = ITestItem.Status.Passed; |
| } else if (testStatusStr.equals(testStatusFailed)) { |
| testStatus = ITestItem.Status.Failed; |
| } else { |
| generateInternalError(MessageFormat.format(GoogleTestsRunnerMessages.OutputHandler_unknown_test_status, testStatusStr)); |
| } |
| String getParamValue = group(7); |
| if (getParamValue != null) { |
| modelUpdater.addTestMessage( |
| DEFAULT_LOCATION_FILE, |
| DEFAULT_LOCATION_LINE, |
| ITestMessage.Level.Info, |
| MessageFormat.format(GoogleTestsRunnerMessages.OutputHandler_getparam_message, getParamValue) |
| ); |
| |
| } |
| modelUpdater.setTestingTime(Integer.parseInt(group(8))); |
| modelUpdater.setTestStatus(testStatus); |
| modelUpdater.exitTestCase(); |
| } |
| } |
| |
| |
| /** |
| * The state is activated when a test suite is finished. |
| */ |
| class TestSuiteEnd extends State { |
| |
| TestSuiteEnd(String enterRegex, int groupCount) { |
| super(enterRegex, groupCount); |
| } |
| |
| /** |
| * Notify Tests Runner Core about test suite end. |
| * |
| * @throws TestingException if current test suite name does not match |
| * last entered test suite name. |
| */ |
| @Override |
| public void onEnter(State previousState) throws TestingException { |
| String lastTestSuiteName = modelUpdater.currentTestSuite().getName(); |
| String currTestSuiteName = getTestSuiteName(group(1), stateTestSuiteStart.getTypeParameter()); |
| if (!lastTestSuiteName.equals(currTestSuiteName)) { |
| generateInternalError( |
| MessageFormat.format( |
| GoogleTestsRunnerMessages.OutputHandler_unexpected_suite_end, |
| currTestSuiteName, lastTestSuiteName |
| ) |
| ); |
| } |
| modelUpdater.exitTestSuite(); |
| } |
| } |
| |
| |
| /** The default file name for test message location. */ |
| private static final String DEFAULT_LOCATION_FILE = null; |
| |
| /** The default line number for test message location. */ |
| private static final int DEFAULT_LOCATION_LINE = 1; |
| |
| // Common regular expression parts |
| static private String regexTestSuiteName = "([^,]+)"; //$NON-NLS-1$ |
| static private String regexParameterInstantiation = "(\\s*,\\s+where\\s+TypeParam\\s*=([^,(]+))?"; //$NON-NLS-1$ |
| static private String regexTestName = regexTestSuiteName+"\\.([^,]+)"; //$NON-NLS-1$ |
| static private String regexTestCount = "\\d+\\s+tests?"; //$NON-NLS-1$ |
| static private String regexTestTime = "(\\d+)\\s+ms"; //$NON-NLS-1$ |
| /* Matches location in the following formats: |
| * - /file:line: |
| * - /file(line): |
| * - /file: (with no line number specified) |
| * Groups: |
| * 1 - all except ":" |
| * 2 - file name (if line present) * |
| * 3 - line number with delimiters |
| * 4 - line number (common style) * |
| * 5 - line number (Visual Studio style) * |
| * 6 - file name (if no line number specified) * |
| * Using: |
| * - group 2 with 4 or 5 (if line number was specified) |
| * - group 6 (if filename only was specified) |
| */ |
| static private String regexLocation = "((.*)(:(\\d+)|\\((\\d+)\\))|(.*[^):])):"; //$NON-NLS-1$ |
| |
| // Test statuses representation |
| static private String testStatusOk = "OK"; //$NON-NLS-1$ |
| static private String testStatusFailed = "FAILED"; //$NON-NLS-1$ |
| |
| |
| // All available states in FSM |
| private State stateInitial = new State(""); //$NON-NLS-1$ |
| private State stateInitialized = new State(".*Global test environment set-up.*"); //$NON-NLS-1$ |
| private TestSuiteStart stateTestSuiteStart = new TestSuiteStart("\\[-*\\]\\s+"+regexTestCount+"\\s+from\\s+"+regexTestSuiteName+regexParameterInstantiation, 3); //$NON-NLS-1$ //$NON-NLS-2$ |
| private State stateTestCaseStart = new TestCaseStart("\\[\\s*RUN\\s*\\]\\s+"+regexTestName, 2); //$NON-NLS-1$ |
| private ErrorMessageLocation stateErrorMessageLocation = new ErrorMessageLocation(regexLocation+"\\s+(Failure|error: (.*))", 8); //$NON-NLS-1$ |
| private State stateErrorMessage = new ErrorMessage("(.*)", 1); //$NON-NLS-1$ |
| private State stateTestTraceStart = new State(".*Google Test trace.*"); //$NON-NLS-1$ |
| // NOTE: Use 8 groups instead of 7 cause we need to be consistent with ErrorMessageLocation (as we subclass it) |
| private State stateTestTrace = new TestTrace(regexLocation+"\\s+((.*))", 8); //$NON-NLS-1$ |
| private State stateTestCaseEnd = new TestCaseEnd("\\[\\s*("+testStatusOk+"|"+testStatusFailed+")\\s*\\]\\s+"+regexTestName+regexParameterInstantiation+"(\\s*,\\s+where\\s+GetParam\\s*\\(\\s*\\)\\s*=\\s*(.+))?\\s+\\("+regexTestTime+"\\)", 8); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ |
| private State stateTestSuiteEnd = new TestSuiteEnd("\\[-*\\]\\s+"+regexTestCount+"\\s+from\\s+"+regexTestSuiteName+"\\s+\\("+regexTestTime+"\\s+total\\)", 2); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ |
| private State stateFinal = new State(".*Global test environment tear-down.*"); //$NON-NLS-1$ |
| // NOTE: This state is a special workaround for empty test modules (they haven't got global test environment set-up/tear-down). They should be always passed. |
| private State stateEmptyTestModuleFinal = new State(".*\\[\\s*PASSED\\s*\\]\\s+0\\s+tests.*"); //$NON-NLS-1$ |
| |
| // Transitions table |
| private Map<State, State[] > transitions = new HashMap<State, State[]>(); |
| { |
| // NOTE: Next states order is important! |
| transitions.put( from(stateInitial), to(stateInitialized, stateEmptyTestModuleFinal) ); |
| transitions.put( from(stateInitialized), to(stateTestSuiteStart) ); |
| transitions.put( from(stateTestSuiteStart), to(stateTestCaseStart) ); |
| transitions.put( from(stateTestCaseStart), to(stateTestCaseEnd, stateErrorMessageLocation) ); |
| transitions.put( from(stateErrorMessageLocation), to(stateTestTraceStart, stateTestCaseEnd, stateErrorMessageLocation, stateErrorMessage) ); |
| transitions.put( from(stateErrorMessage), to(stateTestTraceStart, stateTestCaseEnd, stateErrorMessageLocation, stateErrorMessage) ); |
| transitions.put( from(stateTestTraceStart), to(stateTestTrace) ); |
| transitions.put( from(stateTestTrace), to(stateTestCaseEnd, stateErrorMessageLocation, stateTestTrace) ); |
| transitions.put( from(stateTestCaseEnd), to(stateTestCaseStart, stateTestSuiteEnd) ); |
| transitions.put( from(stateTestSuiteEnd), to(stateTestSuiteStart, stateFinal) ); |
| } |
| |
| /** Current FSM state. */ |
| private State currentState; |
| |
| /** The interface to notify the Tests Runner Core */ |
| private ITestModelUpdater modelUpdater; |
| |
| |
| OutputHandler(ITestModelUpdater modelUpdater) { |
| this.modelUpdater = modelUpdater; |
| } |
| |
| /** |
| * Runs the parsing process. Initializes the FSM, selects new states with |
| * transitions table and checks whether the parsing completes successfully. |
| * |
| * @param inputStream gtest test module output stream |
| * @throws IOException if stream reading error happens |
| * @throws TestingException if testing error happens |
| */ |
| public void run(InputStream inputStream) throws IOException, TestingException { |
| // Initialize input stream reader |
| InputStreamReader streamReader = new InputStreamReader(inputStream); |
| BufferedReader reader = new BufferedReader(streamReader); |
| String line; |
| boolean finalizedProperly = false; |
| |
| // Initialize internal state |
| currentState = stateInitial; |
| while ( ( line = reader.readLine() ) != null ) { |
| // Search for the next possible state |
| State[] possibleNextStates = transitions.get(currentState); |
| if (possibleNextStates == null) { |
| // Final state, stop running |
| finalizedProperly = true; |
| break; |
| } |
| for (State nextState : possibleNextStates) { |
| if (nextState.match(line)) { |
| // Next state found - send notifications to the states |
| currentState.onExit(nextState); |
| State previousState = currentState; |
| currentState = nextState; |
| nextState.onEnter(previousState); |
| break; |
| } |
| } |
| // NOTE: We cannot be sure that we cover all the output of gtest with our regular expressions |
| // (e.g. some framework notes or warnings may be uncovered etc.), so we just skip unmatched |
| // lines without an error |
| } |
| // Check whether the last line leads to the final state |
| if (transitions.get(currentState) == null) { |
| finalizedProperly = true; |
| } |
| if (!finalizedProperly) { |
| generateInternalError(GoogleTestsRunnerMessages.OutputHandler_unexpected_output); |
| } |
| } |
| |
| /** |
| * Throws the testing exception with unknown internal error prefix and the specified description. |
| * |
| * @param additionalInfo additional description of what happens |
| * @throws SAXException the exception that will be thrown |
| */ |
| private void generateInternalError(String additionalInfo) throws TestingException { |
| TestingException e = new TestingException(GoogleTestsRunnerMessages.OutputHandler_unknown_error_prefix+additionalInfo); |
| GoogleTestsRunnerPlugin.log(e); |
| throw e; |
| } |
| |
| /** |
| * Helper functions to make code more readable. |
| * |
| * @param fromState state to return |
| * @return passed state |
| */ |
| private State from(State fromState) { |
| return fromState; |
| } |
| |
| /** |
| * Helper functions to make code more readable. |
| * |
| * @param toStates states array to return |
| * @return passed states array |
| */ |
| private State[] to(State... toStates) { |
| return toStates; |
| } |
| |
| } |