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