1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.cts.tradefed.testtype;
17 
18 import com.android.ddmlib.IShellOutputReceiver;
19 import com.android.ddmlib.Log;
20 import com.android.ddmlib.MultiLineReceiver;
21 import com.android.ddmlib.testrunner.ITestRunListener;
22 import com.android.ddmlib.testrunner.TestIdentifier;
23 import com.android.tradefed.testtype.testdefs.XmlDefsTest;
24 
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 
33 /**
34  * TODO: Merge change to add package prefix to tradefed's GTestResultParser.
35  *       Otherwise this file should be the same as TradeFed's version.
36  *
37  * Parses the 'raw output mode' results of native tests using GTest that run from shell, and informs
38  * a ITestRunListener of the results.
39  * <p>Sample format of output expected:
40  *
41  * <pre>
42  * [==========] Running 15 tests from 1 test case.
43  * [----------] Global test environment set-up.
44  * [----------] 15 tests from MessageTest
45  * [ RUN      ] MessageTest.DefaultConstructor
46  * [       OK ] MessageTest.DefaultConstructor (1 ms)
47  * [ RUN      ] MessageTest.CopyConstructor
48  * external/gtest/test/gtest-message_test.cc:67: Failure
49  * Value of: 5
50  * Expected: 2
51  * external/gtest/test/gtest-message_test.cc:68: Failure
52  * Value of: 1 == 1
53  * Actual: true
54  * Expected: false
55  * [  FAILED  ] MessageTest.CopyConstructor (2 ms)
56  *  ...
57  * [ RUN      ] MessageTest.DoesNotTakeUpMuchStackSpace
58  * [       OK ] MessageTest.DoesNotTakeUpMuchStackSpace (0 ms)
59  * [----------] 15 tests from MessageTest (26 ms total)
60  *
61  * [----------] Global test environment tear-down
62  * [==========] 15 tests from 1 test case ran. (26 ms total)
63  * [  PASSED  ] 6 tests.
64  * [  FAILED  ] 9 tests, listed below:
65  * [  FAILED  ] MessageTest.CopyConstructor
66  * [  FAILED  ] MessageTest.ConstructsFromCString
67  * [  FAILED  ] MessageTest.StreamsCString
68  * [  FAILED  ] MessageTest.StreamsNullCString
69  * [  FAILED  ] MessageTest.StreamsString
70  * [  FAILED  ] MessageTest.StreamsStringWithEmbeddedNUL
71  * [  FAILED  ] MessageTest.StreamsNULChar
72  * [  FAILED  ] MessageTest.StreamsInt
73  * [  FAILED  ] MessageTest.StreamsBasicIoManip
74  * 9 FAILED TESTS
75  * </pre>
76  *
77  * <p>where the following tags are used to signal certain events:
78  * <pre>
79  * [==========]: the first occurrence indicates a new run started, including the number of tests
80  *                  to be expected in this run
81  * [ RUN      ]: indicates a new test has started to run; a series of zero or more lines may
82  *                  follow a test start, and will be captured in case of a test failure or error
83  * [       OK ]: the preceding test has completed successfully, optionally including the time it
84  *                  took to run (in ms)
85  * [  FAILED  ]: the preceding test has failed, optionally including the time it took to run (in ms)
86  * [==========]: the preceding test run has completed, optionally including the time it took to run
87  *                  (in ms)
88  * </pre>
89  *
90  * All other lines are ignored.
91  */
92 public class GeeTestResultParser extends MultiLineReceiver {
93     private static final String LOG_TAG = "GTestResultParser";
94 
95     // Variables to keep track of state
96     private TestResult mCurrentTestResult = null;
97     private int mNumTestsRun = 0;
98     private int mNumTestsExpected = 0;
99     private long mTotalRunTime = 0;
100     private boolean mTestInProgress = false;
101     private boolean mTestRunInProgress = false;
102     private final String mTestRunId;
103     private final Collection<ITestRunListener> mTestListeners;
104 
105     /** Fake adding a package prefix if the test listener needs it. */
106     private String mFakePackagePrefix = "";
107 
108     /** True if start of test has already been reported to listener. */
109     private boolean mTestRunStartReported = false;
110 
111     /** True if current test run has been canceled by user. */
112     private boolean mIsCancelled = false;
113 
114     private String mCoverageTarget = null;
115 
116     /**
117      * Test result data
118      */
119     private static class TestResult {
120         private String mTestName = null;
121         private String mTestClass = null;
122         private StringBuilder mStackTrace = null;
123         @SuppressWarnings("unused")
124         private Long mRunTime = null;
125 
126         /** Returns whether expected values have been parsed
127          *
128          * @return true if all expected values have been parsed
129          */
isComplete()130         boolean isComplete() {
131             return mTestName != null && mTestClass != null;
132         }
133 
134         /** Returns whether there is currently a stack trace
135          *
136          * @return true if there is currently a stack trace, false otherwise
137          */
hasStackTrace()138         boolean hasStackTrace() {
139             return mStackTrace != null;
140         }
141 
142         /**
143          * Returns the stack trace of the current test.
144          *
145          * @return a String representation of the current test's stack trace; if there is not
146          * a current stack trace, it returns an error string. Use {@link TestResult#hasStackTrace}
147          * if you need to know whether there is a stack trace.
148          */
getTrace()149         String getTrace() {
150             if (hasStackTrace()) {
151                 return mStackTrace.toString();
152             } else {
153                 Log.e(LOG_TAG, "Could not find stack trace for failed test");
154                 return new Throwable("Unknown failure").toString();
155             }
156         }
157 
158         /** Provides a more user readable string for TestResult, if possible */
159         @Override
toString()160         public String toString() {
161             StringBuilder output = new StringBuilder();
162             if (mTestClass != null ) {
163                 output.append(mTestClass);
164                 output.append('#');
165             }
166             if (mTestName != null) {
167                 output.append(mTestName);
168             }
169             if (output.length() > 0) {
170                 return output.toString();
171             }
172             return "unknown result";
173         }
174     }
175 
176     /** Internal helper struct to store parsed test info. */
177     private static class ParsedTestInfo {
178         String mTestName = null;
179         String mTestClassName = null;
180         String mTestRunTime = null;
181 
ParsedTestInfo(String testName, String testClassName, String testRunTime)182         public ParsedTestInfo(String testName, String testClassName, String testRunTime) {
183             mTestName = testName;
184             mTestClassName = testClassName;
185             mTestRunTime = testRunTime;
186         }
187     }
188 
189     /** Prefixes used to demarcate and identify output. */
190     private static class Prefixes {
191         @SuppressWarnings("unused")
192         private static final String INFORMATIONAL_MARKER = "[----------]";
193         private static final String START_TEST_RUN_MARKER = "[==========] Running";
194         private static final String TEST_RUN_MARKER = "[==========]";
195         private static final String START_TEST_MARKER = "[ RUN      ]";
196         private static final String OK_TEST_MARKER = "[       OK ]";
197         private static final String FAILED_TEST_MARKER = "[  FAILED  ]";
198     }
199 
200     /**
201      * Creates the GTestResultParser.
202      *
203      * @param testRunId the test run id to provide to
204      *            {@link ITestRunListener#testRunStarted(String, int)}
205      * @param listeners informed of test results as the tests are executing
206      */
GeeTestResultParser(String testRunId, Collection<ITestRunListener> listeners)207     public GeeTestResultParser(String testRunId, Collection<ITestRunListener> listeners) {
208         mTestRunId = testRunId;
209         mTestListeners = new ArrayList<ITestRunListener>(listeners);
210     }
211 
212     /**
213      * Creates the GTestResultParser for a single listener.
214      *
215      * @param testRunId the test run id to provide to
216      *            {@link ITestRunListener#testRunStarted(String, int)}
217      * @param listener informed of test results as the tests are executing
218      */
GeeTestResultParser(String testRunId, ITestRunListener listener)219     public GeeTestResultParser(String testRunId, ITestRunListener listener) {
220         mTestRunId = testRunId;
221         mTestListeners = new ArrayList<ITestRunListener>(1);
222         mTestListeners.add(listener);
223     }
224 
225     /**
226      * Package prefix to be added to test names when they are reported like
227      * "android.nativemedia." You may need to add the dot if you need it.
228      */
setFakePackagePrefix(String prefix)229     public void setFakePackagePrefix(String prefix) {
230         mFakePackagePrefix = prefix;
231     }
232 
233     /**
234      * Returns the current TestResult for test in progress, or a new default one.
235      *
236      * @return The TestResult for the current test run
237      */
getCurrentTestResult()238     private TestResult getCurrentTestResult() {
239         if (mCurrentTestResult == null) {
240             mCurrentTestResult = new TestResult();
241         }
242         return mCurrentTestResult;
243     }
244 
245 
246     /**
247      * Clears out the current TestResult.
248      */
clearCurrentTestResult()249     private void clearCurrentTestResult() {
250         mCurrentTestResult = null;
251     }
252 
253     /**
254      * {@inheritDoc}
255      */
256     @Override
processNewLines(String[] lines)257     public void processNewLines(String[] lines) {
258         for (String line : lines) {
259             parse(line);
260         }
261     }
262 
263     /**
264      * Parse an individual output line.
265      *
266      * @param line  Text output line
267      */
parse(String line)268     private void parse(String line) {
269         String message = null;
270 
271         if (mTestRunInProgress || line.startsWith(Prefixes.TEST_RUN_MARKER)) {
272             if (line.startsWith(Prefixes.START_TEST_MARKER)) {
273                 // Individual test started
274                 message = line.substring(Prefixes.START_TEST_MARKER.length()).trim();
275                 processTestStartedTag(message);
276             }
277             else if (line.startsWith(Prefixes.OK_TEST_MARKER)) {
278                 // Individual test completed successfully
279                 message = line.substring(Prefixes.OK_TEST_MARKER.length()).trim();
280                 processOKTag(message);
281                 clearCurrentTestResult();
282             }
283             else if (line.startsWith(Prefixes.FAILED_TEST_MARKER)) {
284                 // Individual test completed with failure
285                 message = line.substring(Prefixes.FAILED_TEST_MARKER.length()).trim();
286                 processFailedTag(message);
287                 clearCurrentTestResult();
288             }
289             else if (line.startsWith(Prefixes.START_TEST_RUN_MARKER)) {
290                 // Test run started
291                 // Make sure to leave the "Running" in the string
292                 message = line.substring(Prefixes.TEST_RUN_MARKER.length()).trim();
293                 processRunStartedTag(message);
294             }
295             else if (line.startsWith(Prefixes.TEST_RUN_MARKER)) {
296                 // Test run ended
297                 // This is for the end of the test suite run, so make sure this else-if is after the
298                 // check for START_TEST_SUITE_MARKER
299                 message = line.substring(Prefixes.TEST_RUN_MARKER.length()).trim();
300                 processRunCompletedTag(message);
301             }
302             else if (testInProgress()) {
303                 // Note this does not handle the case of an error outside an actual test run
304                 appendTestOutputLine(line);
305             }
306         }
307     }
308 
309     /**
310      * Returns true if test run canceled.
311      *
312      * @see IShellOutputReceiver#isCancelled()
313      */
314     @Override
isCancelled()315     public boolean isCancelled() {
316         return mIsCancelled;
317     }
318 
319     /**
320      * Requests cancellation of test run.
321      */
cancel()322     public void cancel() {
323         mIsCancelled = true;
324     }
325 
326     /**
327      * Returns whether we're in the middle of running a test.
328      *
329      * @return True if a test was started, false otherwise
330      */
testInProgress()331     private boolean testInProgress() {
332         return mTestInProgress;
333     }
334 
335     /**
336      * Set state to indicate we've started running a test.
337      *
338      */
setTestStarted()339     private void setTestStarted() {
340         mTestInProgress = true;
341     }
342 
343     /**
344      * Set state to indicate we've started running a test.
345      *
346      */
setTestEnded()347     private void setTestEnded() {
348         mTestInProgress = false;
349     }
350 
351     /**
352      * Reports the start of a test run, and the total test count, if it has not been previously
353      * reported.
354      */
reportTestRunStarted()355     private void reportTestRunStarted() {
356         // if start test run not reported yet
357         if (!mTestRunStartReported) {
358             for (ITestRunListener listener : mTestListeners) {
359                 listener.testRunStarted(mTestRunId, mNumTestsExpected);
360             }
361             mTestRunStartReported = true;
362         }
363     }
364 
365     /**
366      * Reports the end of a test run, and resets that test
367      */
reportTestRunEnded()368     private void reportTestRunEnded() {
369         for (ITestRunListener listener : mTestListeners) {
370             listener.testRunEnded(mTotalRunTime, getRunMetrics());
371         }
372         mTestRunStartReported = false;
373     }
374 
375     /**
376      * Create the run metrics {@link Map} to report.
377      *
378      * @return a {@link Map} of run metrics data
379      */
getRunMetrics()380     private Map<String, String> getRunMetrics() {
381         Map<String, String> metricsMap = new HashMap<String, String>();
382         if (mCoverageTarget != null) {
383             metricsMap.put(XmlDefsTest.COVERAGE_TARGET_KEY, mCoverageTarget);
384         }
385         return metricsMap;
386     }
387 
388     /**
389      * Parse the test identifier (class and test name), and optional time info.
390      *
391      * @param identifier Raw identifier of the form classname.testname, with an optional time
392      *          element in the format of (XX ms) at the end
393      * @return A ParsedTestInfo representing the parsed info from the identifier string.
394      *
395      *          If no time tag was detected, then the third element in the array (time_in_ms) will
396      *          be null. If the line failed to parse properly (eg: could not determine name of
397      *          test/class) then an "UNKNOWN" string value will be returned for the classname and
398      *          testname. This method guarantees a string will always be returned for the class and
399      *          test names (but not for the time value).
400      */
parseTestIdentifier(String identifier)401     private ParsedTestInfo parseTestIdentifier(String identifier) {
402         ParsedTestInfo returnInfo = new ParsedTestInfo("UNKNOWN_CLASS", "UNKNOWN_TEST", null);
403 
404         Pattern timePattern = Pattern.compile(".*(\\((\\d+) ms\\))");  // eg: (XX ms)
405         Matcher time = timePattern.matcher(identifier);
406 
407         // Try to find a time
408         if (time.find()) {
409             String timeString = time.group(2);  // the "XX" in "(XX ms)"
410             String discardPortion = time.group(1);  // everything after the test class/name
411             identifier = identifier.substring(0, identifier.lastIndexOf(discardPortion)).trim();
412             returnInfo.mTestRunTime = timeString;
413         }
414 
415         String[] testId = identifier.split("\\.");
416         if (testId.length < 2) {
417             Log.e(LOG_TAG, "Could not detect the test class and test name, received: " +
418                     identifier);
419         }
420         else {
421             returnInfo.mTestClassName = testId[0];
422             returnInfo.mTestName = testId[1];
423         }
424         return returnInfo;
425     }
426 
427     /**
428      * Parses and stores the test identifier (class and test name).
429      *
430      * @param identifier Raw identifier
431      */
processRunStartedTag(String identifier)432     private void processRunStartedTag(String identifier) {
433         // eg: (Running XX tests from 1 test case.)
434         Pattern numTestsPattern = Pattern.compile("Running (\\d+) test[s]? from .*");
435         Matcher numTests = numTestsPattern.matcher(identifier);
436 
437         // Try to find number of tests
438         if (numTests.find()) {
439             try {
440                 mNumTestsExpected = Integer.parseInt(numTests.group(1));
441             }
442             catch (NumberFormatException e) {
443                 Log.e(LOG_TAG, "Unable to determine number of tests expected, received: " +
444                         numTests.group(1));
445             }
446         }
447         if (mNumTestsExpected > 0) {
448           reportTestRunStarted();
449           mNumTestsRun = 0;
450           mTestRunInProgress = true;
451         }
452     }
453 
454     /**
455      * Processes and informs listener when we encounter a tag indicating that a test suite is done.
456      *
457      * @param identifier Raw log output from the suite ended tag
458      */
processRunCompletedTag(String identifier)459     private void processRunCompletedTag(String identifier) {
460         Pattern timePattern = Pattern.compile(".*\\((\\d+) ms total\\)");  // eg: (XX ms total)
461         Matcher time = timePattern.matcher(identifier);
462 
463         // Try to find the total run time
464         if (time.find()) {
465             try {
466                 mTotalRunTime = Long.parseLong(time.group(1));
467             }
468             catch (NumberFormatException e) {
469                 Log.e(LOG_TAG, "Unable to determine the total running time, received: " +
470                         time.group(1));
471             }
472         }
473         reportTestRunEnded();
474         mTestRunInProgress = false;
475     }
476 
477     /**
478      * Processes and informs listener when we encounter a tag indicating that a test has started.
479      *
480      * @param identifier Raw log output of the form classname.testname, with an optional time (x ms)
481      */
processTestStartedTag(String identifier)482     private void processTestStartedTag(String identifier) {
483         ParsedTestInfo parsedResults = parseTestIdentifier(identifier);
484         TestResult testResult = getCurrentTestResult();
485         testResult.mTestClass = parsedResults.mTestClassName;
486         testResult.mTestName = parsedResults.mTestName;
487 
488         TestIdentifier testId = new TestIdentifier(mFakePackagePrefix + testResult.mTestClass,
489                 testResult.mTestName);
490 
491         for (ITestRunListener listener : mTestListeners) {
492             listener.testStarted(testId);
493         }
494         setTestStarted();
495     }
496 
497     /**
498      * Helper method to do the work necessary when a test has ended.
499      *
500      * @param identifier Raw log output of the form "classname.testname" with an optional (XX ms)
501      *          at the end indicating the running time.
502      * @param testPassed Indicates whether the test passed or failed (set to true if passed, false
503      *          if failed)
504      */
doTestEnded(String identifier, boolean testPassed)505     private void doTestEnded(String identifier, boolean testPassed) {
506         ParsedTestInfo parsedResults = parseTestIdentifier(identifier);
507         TestResult testResult = getCurrentTestResult();
508         TestIdentifier testId = new TestIdentifier(mFakePackagePrefix + testResult.mTestClass,
509                 testResult.mTestName);
510 
511         // Error - trying to end a test when one isn't in progress
512         if (!testInProgress()) {
513             Log.e(LOG_TAG, "Test currently not in progress when trying to end test: " + identifier);
514             return;
515         }
516 
517         // Save the run time for this test if one exists
518         if (parsedResults.mTestRunTime != null) {
519             try {
520                 testResult.mRunTime = new Long(parsedResults.mTestRunTime);
521             }
522             catch (NumberFormatException e) {
523                 Log.e(LOG_TAG, "Test run time value is invalid, received: " +
524                         parsedResults.mTestRunTime);
525             }
526         }
527 
528         // Check that the test result is for the same test/class we're expecting it to be for
529         boolean encounteredUnexpectedTest = false;
530         if (!testResult.isComplete()) {
531             Log.e(LOG_TAG, "No test/class name is currently recorded as running!");
532         }
533         else {
534             if (testResult.mTestClass.compareTo(parsedResults.mTestClassName) != 0) {
535                 Log.e(LOG_TAG, "Name for current test class does not match class we started " +
536                         "with, expected: " + testResult.mTestClass + " but got: " +
537                         parsedResults.mTestClassName);
538                 encounteredUnexpectedTest = true;
539             }
540             if (testResult.mTestName.compareTo(parsedResults.mTestName) != 0) {
541                 Log.e(LOG_TAG, "Name for current test does not match test we started with," +
542                         "expected: " + testResult.mTestName + " bug got: " +
543                         parsedResults.mTestName);
544                 encounteredUnexpectedTest = true;
545             }
546         }
547 
548         if (encounteredUnexpectedTest) {
549             // If the test name of the result changed from what we started with, report that
550             // the last known test failed, regardless of whether we received a pass or fail tag.
551             for (ITestRunListener listener : mTestListeners) {
552                 listener.testFailed(testId, mCurrentTestResult.getTrace());
553             }
554         }
555         else if (!testPassed) {  // test failed
556             for (ITestRunListener listener : mTestListeners) {
557                 listener.testFailed(testId, mCurrentTestResult.getTrace());
558             }
559         }
560         // For all cases (pass or fail), we ultimately need to report test has ended
561         Map <String, String> emptyMap = Collections.emptyMap();
562         for (ITestRunListener listener : mTestListeners) {
563             // @TODO: Add reporting of test run time to ITestRunListener
564             listener.testEnded(testId, emptyMap);
565         }
566 
567         setTestEnded();
568         ++mNumTestsRun;
569     }
570 
571     /**
572      * Processes and informs listener when we encounter the OK tag.
573      *
574      * @param identifier Raw log output of the form "classname.testname" with an optional (XX ms)
575      *          at the end indicating the running time.
576      */
processOKTag(String identifier)577     private void processOKTag(String identifier) {
578         doTestEnded(identifier, true);
579     }
580 
581     /**
582      * Processes and informs listener when we encounter the FAILED tag.
583      *
584      * @param identifier Raw log output of the form "classname.testname" with an optional (XX ms)
585      *          at the end indicating the running time.
586      */
processFailedTag(String identifier)587     private void processFailedTag(String identifier) {
588         doTestEnded(identifier, false);
589     }
590 
591     /**
592      * Appends the test output to the current TestResult.
593      *
594      * @param line Raw test result line of output.
595      */
appendTestOutputLine(String line)596     private void appendTestOutputLine(String line) {
597         TestResult testResult = getCurrentTestResult();
598         if (testResult.mStackTrace == null) {
599             testResult.mStackTrace = new StringBuilder();
600         }
601         else {
602             testResult.mStackTrace.append("\r\n");
603         }
604         testResult.mStackTrace.append(line);
605     }
606 
607     /**
608      * Process an instrumentation run failure
609      *
610      * @param errorMsg The message to output about the nature of the error
611      */
handleTestRunFailed(String errorMsg)612     private void handleTestRunFailed(String errorMsg) {
613         errorMsg = (errorMsg == null ? "Unknown error" : errorMsg);
614         Log.i(LOG_TAG, String.format("Test run failed: %s", errorMsg));
615         String testRunStackTrace = "";
616 
617         // Report that the last known test failed
618         if ((mCurrentTestResult != null) && (mCurrentTestResult.isComplete())) {
619             // current test results are cleared out after every complete test run,
620             // if it's not null, assume the last test caused this and report as a test failure
621             TestIdentifier testId = new TestIdentifier(mCurrentTestResult.mTestClass,
622                     mCurrentTestResult.mTestName);
623 
624             // If there was any stack trace during the test run, append it to the "test failed"
625             // error message so we have an idea of what caused the crash/failure.
626             Map<String, String> emptyMap = Collections.emptyMap();
627             if (mCurrentTestResult.hasStackTrace()) {
628                 testRunStackTrace = mCurrentTestResult.getTrace();
629             }
630             for (ITestRunListener listener : mTestListeners) {
631                 listener.testFailed(testId, "No test results.\r\n" + testRunStackTrace);
632                 listener.testEnded(testId, emptyMap);
633             }
634             clearCurrentTestResult();
635         }
636         // Report the test run failed
637         for (ITestRunListener listener : mTestListeners) {
638             listener.testRunFailed(errorMsg);
639             listener.testRunEnded(mTotalRunTime, getRunMetrics());
640         }
641     }
642 
643     /**
644      * Called by parent when adb session is complete.
645      */
646     @Override
done()647     public void done() {
648         super.done();
649         if (mNumTestsExpected > mNumTestsRun) {
650             handleTestRunFailed(String.format("Test run incomplete. Expected %d tests, received %d",
651                     mNumTestsExpected, mNumTestsRun));
652         }
653         else if (mTestRunInProgress) {
654             handleTestRunFailed("No test results");
655         }
656     }
657 
658     /**
659      * Sets the coverage target for this test.
660      * <p/>
661      * Will be sent as a metric to test listeners.
662      *
663      * @param coverageTarget the coverage target
664      */
setCoverageTarget(String coverageTarget)665     public void setCoverageTarget(String coverageTarget) {
666         mCoverageTarget = coverageTarget;
667     }
668 }
669