1 package com.android.cts.tradefed.testtype; 2 3 import com.android.cts.tradefed.build.CtsBuildHelper; 4 import com.android.cts.util.AbiUtils; 5 import com.android.ddmlib.MultiLineReceiver; 6 import com.android.ddmlib.testrunner.TestIdentifier; 7 import com.android.tradefed.build.IBuildInfo; 8 import com.android.tradefed.device.DeviceNotAvailableException; 9 import com.android.tradefed.device.ITestDevice; 10 import com.android.tradefed.log.LogUtil.CLog; 11 import com.android.tradefed.result.ByteArrayInputStreamSource; 12 import com.android.tradefed.result.ITestInvocationListener; 13 import com.android.tradefed.result.LogDataType; 14 import com.android.tradefed.testtype.IAbi; 15 import com.android.tradefed.testtype.IBuildReceiver; 16 import com.android.tradefed.testtype.IDeviceTest; 17 import com.android.tradefed.testtype.IRemoteTest; 18 19 import java.io.File; 20 import java.io.FileNotFoundException; 21 import java.util.ArrayList; 22 import java.util.Collection; 23 import java.util.Collections; 24 import java.util.HashMap; 25 import java.util.Iterator; 26 import java.util.Map; 27 28 /** 29 * Test runner for dEQP tests 30 * 31 * Supports running drawElements Quality Program tests found under external/deqp. 32 */ 33 public class DeqpTestRunner implements IBuildReceiver, IDeviceTest, IRemoteTest { 34 35 private static final String DEQP_ONDEVICE_APK = "com.drawelements.deqp.apk"; 36 private static final String DEQP_ONDEVICE_PKG = "com.drawelements.deqp"; 37 private static final String INCOMPLETE_LOG_MESSAGE = "Crash: Incomplete test log"; 38 39 private final int TESTCASE_BATCH_LIMIT = 1000; 40 41 private boolean mLogData; 42 43 private ITestDevice mDevice; 44 45 private final String mPackageName; 46 private final String mName; 47 private Collection<TestIdentifier> mTests; 48 private IAbi mAbi; 49 private CtsBuildHelper mCtsBuild; 50 51 private TestIdentifier mCurrentTestId; 52 private boolean mGotTestResult; 53 private String mCurrentTestLog; 54 55 private ITestInvocationListener mListener; 56 57 public DeqpTestRunner(String packageName, String name, Collection<TestIdentifier> tests) { 58 mPackageName = packageName; 59 mName = name; 60 mTests = tests; 61 mLogData = false; 62 } 63 64 /** 65 * @param abi the ABI to run the test on 66 */ 67 public void setAbi(IAbi abi) { 68 mAbi = abi; 69 } 70 71 /** 72 * {@inheritDoc} 73 */ 74 @Override 75 public void setBuild(IBuildInfo buildInfo) { 76 mCtsBuild = CtsBuildHelper.createBuildHelper(buildInfo); 77 } 78 79 /** 80 * Set the CTS build container. 81 * <p/> 82 * Exposed so unit tests can mock the provided build. 83 * 84 * @param buildHelper 85 */ 86 public void setBuildHelper(CtsBuildHelper buildHelper) { 87 mCtsBuild = buildHelper; 88 } 89 90 /** 91 * Enable or disable raw dEQP test log collection. 92 */ 93 public void setCollectLogs(boolean logData) { 94 mLogData = logData; 95 } 96 97 /** 98 * dEQP instrumentation parser 99 */ 100 class InstrumentationParser extends MultiLineReceiver { 101 private DeqpTestRunner mDeqpTests; 102 103 private Map<String, String> mValues; 104 private String mCurrentName; 105 private String mCurrentValue; 106 107 108 public InstrumentationParser(DeqpTestRunner tests) { 109 mDeqpTests = tests; 110 } 111 112 /** 113 * {@inheritDoc} 114 */ 115 @Override 116 public void processNewLines(String[] lines) { 117 for (String line : lines) { 118 if (mValues == null) mValues = new HashMap<String, String>(); 119 120 if (line.startsWith("INSTRUMENTATION_STATUS_CODE: ")) { 121 if (mCurrentName != null) { 122 mValues.put(mCurrentName, mCurrentValue); 123 124 mCurrentName = null; 125 mCurrentValue = null; 126 } 127 128 mDeqpTests.handleStatus(mValues); 129 mValues = null; 130 } else if (line.startsWith("INSTRUMENTATION_STATUS: dEQP-")) { 131 if (mCurrentName != null) { 132 mValues.put(mCurrentName, mCurrentValue); 133 134 mCurrentValue = null; 135 mCurrentName = null; 136 } 137 138 String prefix = "INSTRUMENTATION_STATUS: "; 139 int nameBegin = prefix.length(); 140 int nameEnd = line.indexOf('='); 141 int valueBegin = nameEnd + 1; 142 143 mCurrentName = line.substring(nameBegin, nameEnd); 144 mCurrentValue = line.substring(valueBegin); 145 } else if (mCurrentValue != null) { 146 mCurrentValue = mCurrentValue + line; 147 } 148 } 149 } 150 151 /** 152 * {@inheritDoc} 153 */ 154 @Override 155 public void done() { 156 if (mCurrentName != null) { 157 mValues.put(mCurrentName, mCurrentValue); 158 159 mCurrentName = null; 160 mCurrentValue = null; 161 } 162 163 if (mValues != null) { 164 mDeqpTests.handleStatus(mValues); 165 mValues = null; 166 } 167 } 168 169 /** 170 * {@inheritDoc} 171 */ 172 @Override 173 public boolean isCancelled() { 174 return false; 175 } 176 } 177 178 /** 179 * Converts dEQP testcase path to TestIdentifier. 180 */ 181 private TestIdentifier pathToIdentifier(String testPath) { 182 String[] components = testPath.split("\\."); 183 String name = components[components.length - 1]; 184 String className = null; 185 186 for (int i = 0; i < components.length - 1; i++) { 187 if (className == null) { 188 className = components[i]; 189 } else { 190 className = className + "." + components[i]; 191 } 192 } 193 194 return new TestIdentifier(className, name); 195 } 196 197 /** 198 * Handles beginning of dEQP session. 199 */ 200 private void handleBeginSession(Map<String, String> values) { 201 String id = AbiUtils.createId(mAbi.getName(), mPackageName); 202 mListener.testRunStarted(id, mTests.size()); 203 } 204 205 /** 206 * Handles end of dEQP session. 207 */ 208 private void handleEndSession(Map<String, String> values) { 209 Map <String, String> emptyMap = Collections.emptyMap(); 210 mListener.testRunEnded(0, emptyMap); 211 } 212 213 /** 214 * Handles beginning of dEQP testcase. 215 */ 216 private void handleBeginTestCase(Map<String, String> values) { 217 mCurrentTestId = pathToIdentifier(values.get("dEQP-BeginTestCase-TestCasePath")); 218 mCurrentTestLog = ""; 219 mGotTestResult = false; 220 221 mListener.testStarted(mCurrentTestId); 222 mTests.remove(mCurrentTestId); 223 } 224 225 /** 226 * Handles end of dEQP testcase. 227 */ 228 private void handleEndTestCase(Map<String, String> values) { 229 Map <String, String> emptyMap = Collections.emptyMap(); 230 231 if (!mGotTestResult) { 232 mListener.testFailed(mCurrentTestId, 233 INCOMPLETE_LOG_MESSAGE); 234 } 235 236 if (mLogData && mCurrentTestLog != null && mCurrentTestLog.length() > 0) { 237 ByteArrayInputStreamSource source 238 = new ByteArrayInputStreamSource(mCurrentTestLog.getBytes()); 239 240 mListener.testLog(mCurrentTestId.getClassName() + "." 241 + mCurrentTestId.getTestName(), LogDataType.XML, source); 242 243 source.cancel(); 244 } 245 246 mListener.testEnded(mCurrentTestId, emptyMap); 247 mCurrentTestId = null; 248 } 249 250 /** 251 * Handles dEQP testcase result. 252 */ 253 private void handleTestCaseResult(Map<String, String> values) { 254 String code = values.get("dEQP-TestCaseResult-Code"); 255 String details = values.get("dEQP-TestCaseResult-Details"); 256 257 if (code.compareTo("Pass") == 0) { 258 mGotTestResult = true; 259 } else if (code.compareTo("NotSupported") == 0) { 260 mGotTestResult = true; 261 } else if (code.compareTo("QualityWarning") == 0) { 262 mGotTestResult = true; 263 } else if (code.compareTo("CompatibilityWarning") == 0) { 264 mGotTestResult = true; 265 } else if (code.compareTo("Fail") == 0 || code.compareTo("ResourceError") == 0 266 || code.compareTo("InternalError") == 0 || code.compareTo("Crash") == 0 267 || code.compareTo("Timeout") == 0) { 268 mListener.testFailed(mCurrentTestId, 269 code + ": " + details); 270 mGotTestResult = true; 271 } else { 272 mListener.testFailed(mCurrentTestId, 273 "Unknown result code: " + code + ": " + details); 274 mGotTestResult = true; 275 } 276 } 277 278 /** 279 * Handles terminated dEQP testcase. 280 */ 281 private void handleTestCaseTerminate(Map<String, String> values) { 282 Map <String, String> emptyMap = Collections.emptyMap(); 283 284 String reason = values.get("dEQP-TerminateTestCase-Reason"); 285 mListener.testFailed(mCurrentTestId, 286 "Terminated: " + reason); 287 mListener.testEnded(mCurrentTestId, emptyMap); 288 289 if (mLogData && mCurrentTestLog != null && mCurrentTestLog.length() > 0) { 290 ByteArrayInputStreamSource source 291 = new ByteArrayInputStreamSource(mCurrentTestLog.getBytes()); 292 293 mListener.testLog(mCurrentTestId.getClassName() + "." 294 + mCurrentTestId.getTestName(), LogDataType.XML, source); 295 296 source.cancel(); 297 } 298 299 mCurrentTestId = null; 300 mGotTestResult = true; 301 } 302 303 /** 304 * Handles dEQP testlog data. 305 */ 306 private void handleTestLogData(Map<String, String> values) { 307 mCurrentTestLog = mCurrentTestLog + values.get("dEQP-TestLogData-Log"); 308 } 309 310 /** 311 * Handles new instrumentation status message. 312 */ 313 public void handleStatus(Map<String, String> values) { 314 String eventType = values.get("dEQP-EventType"); 315 316 if (eventType == null) { 317 return; 318 } 319 320 if (eventType.compareTo("BeginSession") == 0) { 321 handleBeginSession(values); 322 } else if (eventType.compareTo("EndSession") == 0) { 323 handleEndSession(values); 324 } else if (eventType.compareTo("BeginTestCase") == 0) { 325 handleBeginTestCase(values); 326 } else if (eventType.compareTo("EndTestCase") == 0) { 327 handleEndTestCase(values); 328 } else if (eventType.compareTo("TestCaseResult") == 0) { 329 handleTestCaseResult(values); 330 } else if (eventType.compareTo("TerminateTestCase") == 0) { 331 handleTestCaseTerminate(values); 332 } else if (eventType.compareTo("TestLogData") == 0) { 333 handleTestLogData(values); 334 } 335 } 336 337 /** 338 * Generates tescase trie from dEQP testcase paths. Used to define which testcases to execute. 339 */ 340 private String generateTestCaseTrieFromPaths(Collection<String> tests) { 341 String result = "{"; 342 boolean first = true; 343 344 // Add testcases to results 345 for (Iterator<String> iter = tests.iterator(); iter.hasNext();) { 346 String test = iter.next(); 347 String[] components = test.split("\\."); 348 349 if (components.length == 1) { 350 if (!first) { 351 result = result + ","; 352 } 353 first = false; 354 355 result += components[0]; 356 iter.remove(); 357 } 358 } 359 360 if (!tests.isEmpty()) { 361 HashMap<String, ArrayList<String> > testGroups = new HashMap<>(); 362 363 // Collect all sub testgroups 364 for (String test : tests) { 365 String[] components = test.split("\\."); 366 ArrayList<String> testGroup = testGroups.get(components[0]); 367 368 if (testGroup == null) { 369 testGroup = new ArrayList<String>(); 370 testGroups.put(components[0], testGroup); 371 } 372 373 testGroup.add(test.substring(components[0].length()+1)); 374 } 375 376 for (String testGroup : testGroups.keySet()) { 377 if (!first) { 378 result = result + ","; 379 } 380 381 first = false; 382 result = result + testGroup 383 + generateTestCaseTrieFromPaths(testGroups.get(testGroup)); 384 } 385 } 386 387 return result + "}"; 388 } 389 390 /** 391 * Generates testcase trie from TestIdentifiers. 392 */ 393 private String generateTestCaseTrie(Collection<TestIdentifier> tests) { 394 ArrayList<String> testPaths = new ArrayList<String>(); 395 396 for (TestIdentifier test : tests) { 397 testPaths.add(test.getClassName() + "." + test.getTestName()); 398 399 // Limit number of testcases for each run 400 if (testPaths.size() > TESTCASE_BATCH_LIMIT) 401 break; 402 } 403 404 return generateTestCaseTrieFromPaths(testPaths); 405 } 406 407 /** 408 * Executes tests on the device. 409 */ 410 private void executeTests(ITestInvocationListener listener) throws DeviceNotAvailableException { 411 InstrumentationParser parser = new InstrumentationParser(this); 412 String caseListFileName = "/sdcard/dEQP-TestCaseList.txt"; 413 String logFileName = "/sdcard/TestLog.qpa"; 414 String testCases = generateTestCaseTrie(mTests); 415 416 mDevice.executeShellCommand("rm " + caseListFileName); 417 mDevice.executeShellCommand("rm " + logFileName); 418 mDevice.pushString(testCases + "\n", caseListFileName); 419 420 String instrumentationName = 421 "com.drawelements.deqp/com.drawelements.deqp.testercore.DeqpInstrumentation"; 422 423 String command = String.format( 424 "am instrument %s -w -e deqpLogFileName \"%s\" -e deqpCmdLine \"" 425 + "--deqp-caselist-file=%s --deqp-gl-config-name=rgba8888d24s8\"" 426 + " -e deqpLogData \"%s\" %s", 427 AbiUtils.createAbiFlag(mAbi.getName()), logFileName, caseListFileName, mLogData, 428 instrumentationName); 429 430 mDevice.executeShellCommand(command, parser); 431 parser.flush(); 432 } 433 434 /** 435 * Check if device supports OpenGL ES version. 436 */ 437 static boolean isSupportedGles(ITestDevice device, int requiredMajorVersion, int requiredMinorVersion) throws DeviceNotAvailableException { 438 String roOpenglesVersion = device.getProperty("ro.opengles.version"); 439 440 if (roOpenglesVersion == null) 441 return false; 442 443 int intValue = Integer.parseInt(roOpenglesVersion); 444 445 int majorVersion = ((intValue & 0xffff0000) >> 16); 446 int minorVersion = (intValue & 0xffff); 447 448 return (majorVersion > requiredMajorVersion) 449 || (majorVersion == requiredMajorVersion && minorVersion >= requiredMinorVersion); 450 } 451 452 /** 453 * Install dEQP OnDevice Package 454 */ 455 private void installTestApk() throws DeviceNotAvailableException { 456 try { 457 File apkFile = mCtsBuild.getTestApp(DEQP_ONDEVICE_APK); 458 String[] options = {AbiUtils.createAbiFlag(mAbi.getName())}; 459 String errorCode = getDevice().installPackage(apkFile, true, options); 460 if (errorCode != null) { 461 CLog.e("Failed to install %s. Reason: %s", DEQP_ONDEVICE_APK, errorCode); 462 } 463 } catch (FileNotFoundException e) { 464 CLog.e("Could not find test apk %s", DEQP_ONDEVICE_APK); 465 } 466 } 467 468 /** 469 * Uninstall dEQP OnDevice Package 470 */ 471 private void uninstallTestApk() throws DeviceNotAvailableException { 472 getDevice().uninstallPackage(DEQP_ONDEVICE_PKG); 473 } 474 475 /** 476 * {@inheritDoc} 477 */ 478 @Override 479 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 480 mListener = listener; 481 482 if ((mName.equals( "dEQP-GLES3") && isSupportedGles(mDevice, 3, 0)) 483 || (mName.equals("dEQP-GLES31") && isSupportedGles(mDevice, 3, 1))) { 484 485 // Make sure there is no pre-existing package form earlier interrupted test run. 486 uninstallTestApk(); 487 installTestApk(); 488 489 while (!mTests.isEmpty()) { 490 executeTests(listener); 491 492 // Set test to failed if it didn't receive test result 493 if (mCurrentTestId != null) { 494 Map <String, String> emptyMap = Collections.emptyMap(); 495 496 if (mLogData && mCurrentTestLog != null && mCurrentTestLog.length() > 0) { 497 ByteArrayInputStreamSource source 498 = new ByteArrayInputStreamSource(mCurrentTestLog.getBytes()); 499 500 mListener.testLog(mCurrentTestId.getClassName() + "." 501 + mCurrentTestId.getTestName(), LogDataType.XML, source); 502 503 source.cancel(); 504 } 505 if (!mGotTestResult) { 506 mListener.testFailed(mCurrentTestId, 507 INCOMPLETE_LOG_MESSAGE); 508 } 509 510 mListener.testEnded(mCurrentTestId, emptyMap); 511 mCurrentTestId = null; 512 mListener.testRunEnded(0, emptyMap); 513 } 514 } 515 516 uninstallTestApk(); 517 } else { 518 /* Pass all tests if OpenGL ES version is not supported */ 519 Map <String, String> emptyMap = Collections.emptyMap(); 520 String id = AbiUtils.createId(mAbi.getName(), mPackageName); 521 mListener.testRunStarted(id, mTests.size()); 522 523 for (TestIdentifier test : mTests) { 524 CLog.d("Skipping test '%s', Opengl ES version not supported", test.toString()); 525 mListener.testStarted(test); 526 mListener.testEnded(test, emptyMap); 527 } 528 529 mListener.testRunEnded(0, emptyMap); 530 } 531 } 532 533 /** 534 * {@inheritDoc} 535 */ 536 @Override 537 public void setDevice(ITestDevice device) { 538 mDevice = device; 539 } 540 541 /** 542 * {@inheritDoc} 543 */ 544 @Override 545 public ITestDevice getDevice() { 546 return mDevice; 547 } 548 } 549