• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.tradefed.testtype;
17 
18 import com.android.ddmlib.testrunner.ITestRunListener;
19 import com.android.ddmlib.testrunner.TestIdentifier;
20 import com.android.tradefed.log.LogUtil.CLog;
21 
22 import org.json.JSONArray;
23 import org.json.JSONException;
24 import org.json.JSONObject;
25 
26 import java.text.ParseException;
27 import java.text.SimpleDateFormat;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.Date;
32 import java.util.HashMap;
33 import java.util.Map;
34 import java.util.Map.Entry;
35 
36 /**
37  * Interprets the output of tests run with Python's unit test framework and translates it into
38  * calls on a series of {@link ITestRunListener}s. Output from these tests follows this
39  * EBNF grammar:
40  */
41 public class VtsMultiDeviceTestResultParser {
42 
43     // Parser state
44     String[] mAllLines;
45     String mCurrentLine;
46     int mLineNum;
47     ParserState mCurrentParseState = ParserState.TEST_CASE_ENTRY;
48 
49     // Current test state
50     String mTestClass;
51     TestIdentifier mCurrentTestId;
52     String mCurrentTraceback;
53     long mTotalElapsedTime = 0;
54 
55     // General state
56     private Map<TestIdentifier, String> mTestResultCache;
57     private final Collection<ITestRunListener> mListeners;
58     private String mRunName = null;
59     private String mCurrentTestName = null;
60     private int mTotalTestCount = 0;
61 
62     // Variables to keep track of state for unit test
63     private int mNumTestsRun = 0;
64     private int mNumTestsExpected = 1;
65 
66     // Constant tokens that appear in the result grammar.
67     static final String CASE_OK = "ok";
68     static final String RUN_OK = "OK";
69     static final String RUN_FAILED = "FAILED";
70     static final String TEST_CASE = "[Test Case]";
71     static final String PASS = "PASS";
72     static final String FAIL = "FAIL";
73     static final String TIMEOUT = "TIMEOUT";
74     static final String SKIP = "SKIP";
75     static final String END_PATTERN = "<==========";
76     static final String BEGIN_PATTERN = "==========>";
77 
78     // constants for parsing json file
79     static final String RESULTS = "Results";
80     static final String BEGIN_TIME = "Begin Time";
81     static final String DETAILS = "Details";
82     static final String END_TIME = "End Time";
83     static final String TEST_CLASS = "Test Class";
84     static final String TEST_NAME = "Test Name";
85     static final String RESULT = "Result";
86 
87     // default message for test failure
88     static final String UNKNOWN_ERROR = "Unknown error.";
89 
90     // Enumeration for parser state.
91     static enum ParserState {
92         TEST_CASE_ENTRY,
93         COMPLETE,
94         TEST_RUN
95     }
96 
97     private class PythonUnitTestParseException extends Exception {
PythonUnitTestParseException(String reason)98         public PythonUnitTestParseException(String reason) {
99             super(reason);
100         }
101     }
102 
VtsMultiDeviceTestResultParser(ITestRunListener listener, String runName)103     public VtsMultiDeviceTestResultParser(ITestRunListener listener,
104             String runName) {
105         mListeners = new ArrayList<ITestRunListener>(1);
106         mListeners.add(listener);
107         mRunName = runName;
108         mTestResultCache = new HashMap<>();
109     }
110 
processNewLines(String[] lines)111     public void processNewLines(String[] lines) {
112         init(lines);
113 
114         // extract the test class name if pattern matches for the first line
115         String[] toks = mCurrentLine.split(" ");
116         if (toks.length < 3 || !toks[toks.length - 1].equals(END_PATTERN) || !toks[toks.length - 3].
117                 equals(BEGIN_PATTERN)) {
118             try {
119                 parseError(BEGIN_PATTERN);
120             } catch (PythonUnitTestParseException e) {
121                 e.printStackTrace();
122             }
123         } else {
124             mTestClass = toks[toks.length - 2];
125         }
126 
127         // parse all lines
128         for (String line : lines) {
129             if (line.length() == 0 || !line.contains(TEST_CASE)) {
130                 continue;
131             }
132             mCurrentLine = line;
133             try {
134                 parse();
135             } catch (PythonUnitTestParseException e) {
136                 e.printStackTrace();
137             }
138         }
139 
140         // current state should not be in TEST_RUN state; if it's then mark for timeout.
141         if (mCurrentParseState.equals(ParserState.TEST_RUN)) {
142             markTestTimeout();
143         }
144         summary(lines);
145         completeTestRun();
146         mNumTestsRun++;
147     }
148 
149     /**
150      * Called by parent when adb session is complete.
151      */
done()152     public void done() {
153         if (mNumTestsRun < mNumTestsExpected) {
154             handleTestRunFailed(String.format("Test run failed. Expected %d tests, received %d",
155                     mNumTestsExpected, mNumTestsRun));
156         }
157     }
158 
159     /**
160      * Process an instrumentation run failure
161      *
162      * @param errorMsg The message to output about the nature of the error
163      */
handleTestRunFailed(String errorMsg)164     private void handleTestRunFailed(String errorMsg) {
165         errorMsg = (errorMsg == null ? "Unknown error" : errorMsg);
166         CLog.e(String.format("Test run failed: %s", errorMsg));
167 
168         Map<String, String> emptyMap = Collections.emptyMap();
169         for (ITestRunListener listener : mListeners) {
170             listener.testFailed(mCurrentTestId, FAIL);
171             listener.testEnded(mCurrentTestId, emptyMap);
172         }
173 
174         // Report the test run failed
175         for (ITestRunListener listener : mListeners) {
176             listener.testRunFailed(errorMsg);
177         }
178     }
179 
init(String[] lines)180     void init(String[] lines) {
181         mAllLines = lines;
182         mLineNum = 0;
183         mCurrentLine = mAllLines[0];
184     }
185 
186     /**
187      * This method parses individual lines and calls functions based on the parsing state.
188      * @throws PythonUnitTestParseException
189      */
parse()190     void parse() throws PythonUnitTestParseException {
191         try {
192             switch(mCurrentParseState) {
193                 case TEST_CASE_ENTRY:
194                     registerTest();
195                     mCurrentParseState = ParserState.TEST_RUN;
196                     break;
197                 case TEST_RUN:
198                     if (testRun()) {
199                         mCurrentParseState = ParserState.COMPLETE;
200                     } else {
201                         // incomplete test due to timeout.
202                         mCurrentParseState = ParserState.TEST_CASE_ENTRY;
203                         parse();
204                     }
205                     break;
206                 case COMPLETE:
207                     mCurrentParseState = ParserState.TEST_CASE_ENTRY;
208                     parse();
209                     break;
210                 default:
211                     break;
212             }
213         } catch (ArrayIndexOutOfBoundsException e) {
214             CLog.d("Underlying error in testResult: " + e);
215             throw new PythonUnitTestParseException("FailMessage");
216         }
217     }
218 
219     /**
220      * This is called whenever the parser state is {@link ParserState#TEST_RUN}
221      */
testRun()222     private boolean testRun() {
223         // process the test case
224         String[] toks = mCurrentLine.split(" ");
225         // check the last token
226         String lastToken = toks[toks.length - 1];
227         if (lastToken.equals(PASS)){
228             markTestSuccess();
229             return true;
230         } else if (lastToken.equals(FAIL)){
231             markTestFailure();
232             return true;
233         } else if (lastToken.equals(SKIP)){
234             // SKIP is not a recognized test type in TradeFed.
235             return true;
236         } else {
237             markTestTimeout();
238             return false;
239         }
240     }
241 
242     /**
243      * This method is called when parser is at {@link ParserState#TEST_CASE_ENTRY}} stage and
244      * this registers a new test case.
245      */
registerTest()246     private void registerTest() {
247         // process the test case
248         String[] toks = mCurrentLine.split(" ");
249         mCurrentTestName = toks[toks.length - 1];
250         mCurrentTestId = new TestIdentifier(mTestClass, mCurrentTestName);
251         mTotalTestCount++;
252     }
253     /**
254      * Called at the end of the parsing to calculate the {@link #mTotalElapsedTime}
255      *
256      * @param lines The corresponding logs.
257      */
summary(String[] lines)258     void summary(String[] lines) {
259         if (lines == null || lines.length == 0) {
260             mTotalElapsedTime = 0;
261             return;
262         }
263 
264         Date startDate = getDate(lines, true);
265         Date endDate = getDate(lines, false);
266 
267         if (startDate == null || endDate == null) {
268             mTotalElapsedTime = 0;
269             return;
270         }
271         mTotalElapsedTime = endDate.getTime() - startDate.getTime();
272     }
273 
274     /**
275      * Return the time in milliseconds to calculate the time elapsed in a particular test.
276      *
277      * @param lines The logs that need to be parsed.
278      * @param calculateStartDate flag which is true if we need to calculate start date.
279      * @return {Date} the start and end time corresponding to a test.
280      */
getDate(String[] lines, boolean calculateStartDate)281     private Date getDate(String[] lines, boolean calculateStartDate) {
282         Date date = null;
283         int begin = calculateStartDate ? 0 : lines.length - 1;
284         int diff = calculateStartDate ? 1 : -1;
285 
286         for (int index = begin; index >= 0 && index < lines.length; index += diff) {
287             lines[index].trim();
288             String[] toks = lines[index].split(" ");
289 
290             // set the start time from the first line
291             // the loop should continue if exception occurs, else it can break
292             if (toks.length < 3) {
293                 continue;
294             }
295             String time = toks[2];
296             SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
297             try {
298                 date = sdf.parse(time);
299             } catch (ParseException e) {
300                 continue;
301             }
302             break;
303         }
304         return date;
305     }
306 
completeTestRun()307     boolean completeTestRun() {
308         for (ITestRunListener listener: mListeners) {
309             // do testRunStarted
310             listener.testRunStarted(mCurrentTestName, mTotalTestCount);
311 
312             // mark each test passed or failed
313             for (Entry<TestIdentifier, String> test : mTestResultCache.entrySet()) {
314                 listener.testStarted(test.getKey());
315                 if (test.getValue() == PASS) {
316                     listener.testEnded(test.getKey(), Collections.<String, String>emptyMap());
317                 } else if (test.getValue() == TIMEOUT) {
318                     listener.testFailed(test.getKey(), test.getValue());
319                 } else if (test.getValue() == SKIP) {
320                     listener.testAssumptionFailure(test.getKey(), test.getValue());
321                 } else {
322                     listener.testFailed(test.getKey(), test.getValue());
323                 }
324             }
325             listener.testRunEnded(mTotalElapsedTime, Collections.<String, String>emptyMap());
326         }
327         return true;
328     }
329 
330     /**
331      * This method parses the json object and summarizes the test result through listener.
332      * @param object
333      * @throws RuntimeException
334      */
processJsonFile(JSONObject object)335     public void processJsonFile(JSONObject object) throws RuntimeException{
336         long beginTime = -1, endTime = -1;
337         JSONArray results = null;
338 
339         try {
340             results = object.getJSONArray(RESULTS);
341             for (ITestRunListener listener: mListeners) {
342                 if (results == null || results.length() < 1) {
343                     CLog.e("JSONArray is null.");
344                     continue;
345                 }
346                 // calculate test run time
347                 beginTime = (long) results.getJSONObject(0).get(BEGIN_TIME);
348                 endTime = (long) results.getJSONObject(results.length() - 1).get(END_TIME);
349 
350                 // do testRunStarted
351                 listener.testRunStarted((String) results.getJSONObject(0).get(TEST_CLASS),
352                         results.length());
353 
354                 for (int index = 0; index < results.length(); index++) {
355                     JSONObject  resultObject = results.getJSONObject(index);
356                     String result = (String) resultObject.get(RESULT);
357                     String testClass = (String) resultObject.get(TEST_CLASS);
358                     String testName = (String) resultObject.get(TEST_NAME);
359                     String details =
360                             resultObject.isNull(DETAILS) ? "" : resultObject.getString(DETAILS);
361 
362                     // mark test started
363                     TestIdentifier testIdentifier = new TestIdentifier(testClass, testName);
364                     listener.testStarted(testIdentifier);
365 
366                     switch (result) {
367                         case PASS :
368                             listener.testEnded(testIdentifier, Collections.<String, String>emptyMap());
369                             break;
370                         case TIMEOUT :
371                             /* Timeout is not recognized in TF*/
372                             break;
373                         case SKIP :
374                             /* Skip is not recognized in TF*/
375                             break;
376                         case FAIL:
377                             listener.testFailed(
378                                     testIdentifier, details.isEmpty() ? UNKNOWN_ERROR : details);
379                         default:
380                             break;
381                     }
382                 }
383                 listener.testRunEnded(endTime - beginTime, Collections.<String, String>emptyMap());
384             }
385         } catch (JSONException e) {
386             CLog.e("Exception occurred %s :", e);
387         }
388     }
389 
390     /**
391      * This is called whenever the program encounters unexpected tokens in parsing.
392      *
393      * @param expected The string that was expected.
394      * @throws PythonUnitTestParseException
395      */
parseError(String expected)396     private void parseError(String expected)
397             throws PythonUnitTestParseException {
398         throw new PythonUnitTestParseException(
399                 String.format("Expected \"%s\" on line %d, found %s instead",
400                         expected, mLineNum + 1, mCurrentLine));
401     }
402 
markTestSuccess()403     private void markTestSuccess() {
404         mTestResultCache.put(mCurrentTestId, PASS);
405     }
406 
markTestFailure()407     private void markTestFailure() {
408         mTestResultCache.put(mCurrentTestId, FAIL);
409     }
410 
411     /**
412      * This method is called whenever the current test doesn't finish as expected and runs out
413      * of time.
414      */
markTestTimeout()415     private void markTestTimeout() {
416         mTestResultCache.put(mCurrentTestId, TIMEOUT);
417     }
418 
isCancelled()419     public boolean isCancelled() {
420         return false;
421     }
422 }
423