1 /* 2 * Copyright (C) 2014 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.drawelements.deqp.runner; 17 18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 19 import com.android.compatibility.common.tradefed.targetprep.IncrementalDeqpPreparer; 20 import com.android.compatibility.common.util.PropertyUtil; 21 import com.android.ddmlib.AdbCommandRejectedException; 22 import com.android.ddmlib.IShellOutputReceiver; 23 import com.android.ddmlib.MultiLineReceiver; 24 import com.android.ddmlib.ShellCommandUnresponsiveException; 25 import com.android.ddmlib.TimeoutException; 26 import com.android.tradefed.build.IBuildInfo; 27 import com.android.tradefed.config.Option; 28 import com.android.tradefed.config.OptionClass; 29 import com.android.tradefed.device.DeviceNotAvailableException; 30 import com.android.tradefed.device.IManagedTestDevice; 31 import com.android.tradefed.device.ITestDevice; 32 import com.android.tradefed.error.HarnessRuntimeException; 33 import com.android.tradefed.log.LogUtil.CLog; 34 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 35 import com.android.tradefed.result.ByteArrayInputStreamSource; 36 import com.android.tradefed.result.ITestInvocationListener; 37 import com.android.tradefed.result.LogDataType; 38 import com.android.tradefed.result.TestDescription; 39 import com.android.tradefed.result.error.TestErrorIdentifier; 40 import com.android.tradefed.testtype.IAbi; 41 import com.android.tradefed.testtype.IAbiReceiver; 42 import com.android.tradefed.testtype.IBuildReceiver; 43 import com.android.tradefed.testtype.IDeviceTest; 44 import com.android.tradefed.testtype.IRemoteTest; 45 import com.android.tradefed.testtype.IRuntimeHintProvider; 46 import com.android.tradefed.testtype.IShardableTest; 47 import com.android.tradefed.testtype.ITestCollector; 48 import com.android.tradefed.testtype.ITestFilterReceiver; 49 import com.android.tradefed.util.AbiUtils; 50 import com.android.tradefed.util.FileUtil; 51 import com.android.tradefed.util.IRunUtil; 52 import com.android.tradefed.util.RunInterruptedException; 53 import com.android.tradefed.util.RunUtil; 54 55 import java.io.BufferedReader; 56 import java.io.File; 57 import java.io.FileNotFoundException; 58 import java.io.FileReader; 59 import java.io.IOException; 60 import java.io.Reader; 61 import java.nio.file.Paths; 62 import java.util.ArrayList; 63 import java.util.Collection; 64 import java.util.HashMap; 65 import java.util.HashSet; 66 import java.util.Iterator; 67 import java.util.LinkedHashMap; 68 import java.util.LinkedHashSet; 69 import java.util.List; 70 import java.util.Map; 71 import java.util.Optional; 72 import java.util.Set; 73 import java.util.concurrent.TimeUnit; 74 import java.util.regex.Matcher; 75 import java.util.regex.Pattern; 76 77 /** 78 * Test runner for dEQP tests 79 * 80 * Supports running drawElements Quality Program tests found under external/deqp. 81 */ 82 @OptionClass(alias="deqp-test-runner") 83 public class DeqpTestRunner implements IBuildReceiver, IDeviceTest, 84 ITestFilterReceiver, IAbiReceiver, IShardableTest, ITestCollector, 85 IRuntimeHintProvider { 86 private static final String DEQP_ONDEVICE_APK = "com.drawelements.deqp.apk"; 87 private static final String DEQP_ONDEVICE_PKG = "com.drawelements.deqp"; 88 private static final String INCOMPLETE_LOG_MESSAGE = "Crash: Incomplete test log"; 89 private static final String TIMEOUT_LOG_MESSAGE = "Timeout: Test timeout"; 90 private static final String SKIPPED_INSTANCE_LOG_MESSAGE = "Configuration skipped"; 91 private static final String NOT_EXECUTABLE_LOG_MESSAGE = "Abort: Test cannot be executed"; 92 private static final String APP_DIR = "/sdcard/"; 93 private static final String CASE_LIST_FILE_NAME = "dEQP-TestCaseList.txt"; 94 private static final String LOG_FILE_NAME = "TestLog.qpa"; 95 public static final String FEATURE_LANDSCAPE = "android.hardware.screen.landscape"; 96 public static final String FEATURE_PORTRAIT = "android.hardware.screen.portrait"; 97 public static final String FEATURE_VULKAN_LEVEL = "android.hardware.vulkan.level"; 98 public static final String FEATURE_VULKAN_DEQP_LEVEL = "android.software.vulkan.deqp.level"; 99 public static final String FEATURE_OPENGLES_DEQP_LEVEL = "android.software.opengles.deqp.level"; 100 101 private static final int TESTCASE_BATCH_LIMIT = 1000; 102 private static final int TESTCASE_BATCH_LIMIT_LARGE = 10000; 103 private static final int UNRESPONSIVE_CMD_TIMEOUT_MS_DEFAULT = 10 * 60 * 1000; // 10min 104 private static final int R_API_LEVEL = 30; 105 private static final int DEQP_LEVEL_R_2020 = 132383489; 106 107 private static final String ANGLE_NONE = "none"; 108 private static final String ANGLE_VULKAN = "vulkan"; 109 private static final String ANGLE_OPENGLES = "opengles"; 110 111 // !NOTE: There's a static method copyOptions() for copying options during split. 112 // If you add state update copyOptions() as appropriate! 113 114 @Option(name="timeout", 115 description="Timeout for unresponsive tests in milliseconds. Default: " + UNRESPONSIVE_CMD_TIMEOUT_MS_DEFAULT, 116 importance=Option.Importance.NEVER) 117 private long mUnresponsiveCmdTimeoutMs = UNRESPONSIVE_CMD_TIMEOUT_MS_DEFAULT; 118 @Option(name="deqp-package", 119 description="Name of the deqp module used. Determines GLES version.", 120 importance=Option.Importance.ALWAYS) 121 private String mDeqpPackage; 122 @Option(name="deqp-gl-config-name", 123 description="GL render target config. See deqp documentation for syntax. ", 124 importance=Option.Importance.NEVER) 125 private String mConfigName = ""; 126 @Option(name="deqp-caselist-file", 127 description="File listing the names of the cases to be run.", 128 importance=Option.Importance.ALWAYS) 129 private String mCaselistFile; 130 @Option(name="deqp-screen-rotation", 131 description="Screen orientation. Defaults to 'unspecified'", 132 importance=Option.Importance.NEVER) 133 private String mScreenRotation = "unspecified"; 134 @Option(name="deqp-surface-type", 135 description="Surface type ('window', 'pbuffer', 'fbo'). Defaults to 'window'", 136 importance=Option.Importance.NEVER) 137 private String mSurfaceType = "window"; 138 @Option(name="deqp-config-required", 139 description="Is current config required if API is supported? Defaults to false.", 140 importance=Option.Importance.NEVER) 141 private boolean mConfigRequired = false; 142 @Option(name = "include-filter", 143 description="Test include filter. '*' is zero or more letters. '.' has no special meaning.") 144 private List<String> mIncludeFilters = new ArrayList<>(); 145 @Option(name = "include-filter-file", 146 description="Load list of includes from the files given.") 147 private List<String> mIncludeFilterFiles = new ArrayList<>(); 148 @Option(name = "exclude-filter", 149 description="Test exclude filter. '*' is zero or more letters. '.' has no special meaning.") 150 private List<String> mExcludeFilters = new ArrayList<>(); 151 @Option(name = "exclude-filter-file", 152 description="Load list of excludes from the files given.") 153 private List<String> mExcludeFilterFiles = new ArrayList<>(); 154 @Option(name = "incremental-deqp-include-file", 155 description="Load list of includes from the files given for incremental dEQP.") 156 private List<String> mIncrementalDeqpIncludeFiles = new ArrayList<>(); 157 @Option(name = "collect-tests-only", 158 description = "Only invoke the instrumentation to collect list of applicable test " 159 + "cases. All test run callbacks will be triggered, but test execution will " 160 + "not be actually carried out.") 161 private boolean mCollectTestsOnly = false; 162 @Option(name = "runtime-hint", 163 isTimeVal = true, 164 description="The estimated config runtime. Defaults to 200ms x num tests.") 165 private long mRuntimeHint = -1; 166 167 @Option(name="deqp-use-angle", 168 description="ANGLE backend ('none', 'vulkan', 'opengles'). Defaults to 'none' (don't use ANGLE)", 169 importance=Option.Importance.NEVER) 170 private String mAngle = "none"; 171 172 @Option( 173 name = "disable-watchdog", 174 description = 175 "Disable the native testrunner's per-test watchdog.") 176 private boolean mDisableWatchdog = false; 177 178 private Set<TestDescription> mRemainingTests = null; 179 private Map<TestDescription, Set<BatchRunConfiguration>> mTestInstances = null; 180 private final TestInstanceResultListener mInstanceListerner = new TestInstanceResultListener(); 181 private final Map<TestDescription, Integer> mTestInstabilityRatings = new HashMap<>(); 182 private IAbi mAbi; 183 private CompatibilityBuildHelper mBuildHelper; 184 private boolean mLogData = false; 185 private ITestDevice mDevice; 186 private Map<String, Optional<Integer>> mDeviceFeatures; 187 private Map<String, Boolean> mConfigQuerySupportCache = new HashMap<>(); 188 private IRunUtil mRunUtil = RunUtil.getDefault(); 189 private Set<String> mIncrementalDeqpIncludeTests = new HashSet<>(); 190 private long mTimeOfLastRun = 0; 191 192 private IRecovery mDeviceRecovery = new Recovery(); { mDeviceRecovery.setSleepProvider(new SleepProvider())193 mDeviceRecovery.setSleepProvider(new SleepProvider()); 194 } 195 DeqpTestRunner()196 public DeqpTestRunner() { 197 } 198 DeqpTestRunner(DeqpTestRunner optionTemplate, Map<TestDescription, Set<BatchRunConfiguration>> tests)199 private DeqpTestRunner(DeqpTestRunner optionTemplate, 200 Map<TestDescription, Set<BatchRunConfiguration>> tests) { 201 copyOptions(this, optionTemplate); 202 mTestInstances = tests; 203 } 204 205 /** 206 * @param abi the ABI to run the test on 207 */ 208 @Override setAbi(IAbi abi)209 public void setAbi(IAbi abi) { 210 mAbi = abi; 211 } 212 213 @Override getAbi()214 public IAbi getAbi() { 215 return mAbi; 216 } 217 218 /** 219 * {@inheritDoc} 220 */ 221 @Override setBuild(IBuildInfo buildInfo)222 public void setBuild(IBuildInfo buildInfo) { 223 setBuildHelper(new CompatibilityBuildHelper(buildInfo)); 224 } 225 226 /** 227 * Exposed for better mockability during testing. In real use, always flows from 228 * setBuild() called by the framework 229 */ setBuildHelper(CompatibilityBuildHelper helper)230 public void setBuildHelper(CompatibilityBuildHelper helper) { 231 mBuildHelper = helper; 232 } 233 234 /** 235 * Enable or disable raw dEQP test log collection. 236 */ setCollectLogs(boolean logData)237 public void setCollectLogs(boolean logData) { 238 mLogData = logData; 239 } 240 241 /** 242 * Get the deqp-package option contents. 243 */ getPackageName()244 public String getPackageName() { 245 return mDeqpPackage; 246 } 247 248 /** 249 * {@inheritDoc} 250 */ 251 @Override setDevice(ITestDevice device)252 public void setDevice(ITestDevice device) { 253 mDevice = device; 254 } 255 256 /** 257 * {@inheritDoc} 258 */ 259 @Override getDevice()260 public ITestDevice getDevice() { 261 return mDevice; 262 } 263 264 /** 265 * Set recovery handler. 266 * 267 * Exposed for unit testing. 268 */ setRecovery(IRecovery deviceRecovery)269 public void setRecovery(IRecovery deviceRecovery) { 270 mDeviceRecovery = deviceRecovery; 271 } 272 273 /** 274 * Set IRunUtil. 275 * 276 * Exposed for unit testing. 277 */ setRunUtil(IRunUtil runUtil)278 public void setRunUtil(IRunUtil runUtil) { 279 mRunUtil = runUtil; 280 } 281 282 private static final class CapabilityQueryFailureException extends Exception { 283 } 284 285 /** 286 * dEQP test instance listerer and invocation result forwarded 287 */ 288 private class TestInstanceResultListener { 289 private ITestInvocationListener mSink; 290 private BatchRunConfiguration mRunConfig; 291 292 private TestDescription mCurrentTestId; 293 private boolean mGotTestResult; 294 private String mCurrentTestLog; 295 296 private class PendingResult { 297 boolean allInstancesPassed; 298 Map<BatchRunConfiguration, String> testLogs; 299 Map<BatchRunConfiguration, String> errorMessages; 300 Set<BatchRunConfiguration> remainingConfigs; 301 } 302 303 private final Map<TestDescription, PendingResult> mPendingResults = new HashMap<>(); 304 setSink(ITestInvocationListener sink)305 public void setSink(ITestInvocationListener sink) { 306 mSink = sink; 307 } 308 setCurrentConfig(BatchRunConfiguration runConfig)309 public void setCurrentConfig(BatchRunConfiguration runConfig) { 310 mRunConfig = runConfig; 311 } 312 313 /** 314 * Get currently processed test id, or null if not currently processing a test case 315 */ getCurrentTestId()316 public TestDescription getCurrentTestId() { 317 return mCurrentTestId; 318 } 319 320 /** 321 * Forward result to sink 322 */ forwardFinalizedPendingResult(TestDescription testId)323 private void forwardFinalizedPendingResult(TestDescription testId) { 324 if (mRemainingTests.contains(testId)) { 325 final PendingResult result = mPendingResults.get(testId); 326 327 mPendingResults.remove(testId); 328 mRemainingTests.remove(testId); 329 330 // Forward results to the sink 331 mSink.testStarted(testId); 332 333 // Test Log 334 if (mLogData) { 335 for (Map.Entry<BatchRunConfiguration, String> entry : 336 result.testLogs.entrySet()) { 337 final ByteArrayInputStreamSource source 338 = new ByteArrayInputStreamSource(entry.getValue().getBytes()); 339 340 mSink.testLog(testId.getClassName() + "." + testId.getTestName() + "@" 341 + entry.getKey().getId(), LogDataType.XML, source); 342 343 source.close(); 344 } 345 } 346 347 // Error message 348 if (!result.allInstancesPassed) { 349 final StringBuilder errorLog = new StringBuilder(); 350 351 for (Map.Entry<BatchRunConfiguration, String> entry : 352 result.errorMessages.entrySet()) { 353 if (errorLog.length() > 0) { 354 errorLog.append('\n'); 355 } 356 errorLog.append(String.format("=== with config %s ===\n", 357 entry.getKey().getId())); 358 errorLog.append(entry.getValue()); 359 } 360 361 mSink.testFailed(testId, errorLog.toString()); 362 } 363 364 final HashMap<String, Metric> emptyMap = new HashMap<>(); 365 mSink.testEnded(testId, emptyMap); 366 } 367 } 368 369 /** 370 * Declare existence of a test and instances 371 */ setTestInstances(TestDescription testId, Set<BatchRunConfiguration> configs)372 public void setTestInstances(TestDescription testId, Set<BatchRunConfiguration> configs) { 373 // Test instances cannot change at runtime, ignore if we have already set this 374 if (!mPendingResults.containsKey(testId)) { 375 final PendingResult pendingResult = new PendingResult(); 376 pendingResult.allInstancesPassed = true; 377 pendingResult.testLogs = new LinkedHashMap<>(); 378 pendingResult.errorMessages = new LinkedHashMap<>(); 379 pendingResult.remainingConfigs = new HashSet<>(configs); // avoid mutating argument 380 mPendingResults.put(testId, pendingResult); 381 } 382 } 383 384 /** 385 * Query if test instance has not yet been executed 386 */ isPendingTestInstance(TestDescription testId, BatchRunConfiguration config)387 public boolean isPendingTestInstance(TestDescription testId, 388 BatchRunConfiguration config) { 389 final PendingResult result = mPendingResults.get(testId); 390 if (result == null) { 391 // test is not in the current working batch of the runner, i.e. it cannot be 392 // "partially" completed. 393 if (!mRemainingTests.contains(testId)) { 394 // The test has been fully executed. Not pending. 395 return false; 396 } else { 397 // Test has not yet been executed. Check if such instance exists 398 return mTestInstances.get(testId).contains(config); 399 } 400 } else { 401 // could be partially completed, check this particular config 402 return result.remainingConfigs.contains(config); 403 } 404 } 405 406 /** 407 * Fake execution of an instance with current config 408 */ skipTest(TestDescription testId)409 public void skipTest(TestDescription testId) { 410 final PendingResult result = mPendingResults.get(testId); 411 412 result.errorMessages.put(mRunConfig, SKIPPED_INSTANCE_LOG_MESSAGE); 413 result.remainingConfigs.remove(mRunConfig); 414 415 // Pending result finished, report result 416 if (result.remainingConfigs.isEmpty()) { 417 forwardFinalizedPendingResult(testId); 418 } 419 } 420 421 /** 422 * Fake failure of an instance with current config 423 */ abortTest(TestDescription testId, String errorMessage)424 public void abortTest(TestDescription testId, String errorMessage) { 425 final PendingResult result = mPendingResults.get(testId); 426 427 // Mark as executed 428 result.allInstancesPassed = false; 429 result.errorMessages.put(mRunConfig, errorMessage); 430 result.remainingConfigs.remove(mRunConfig); 431 432 // Pending result finished, report result 433 if (result.remainingConfigs.isEmpty()) { 434 forwardFinalizedPendingResult(testId); 435 } 436 437 if (testId.equals(mCurrentTestId)) { 438 mCurrentTestId = null; 439 } 440 } 441 442 /** 443 * Handles beginning of dEQP session. 444 */ handleBeginSession(Map<String, String> values)445 private void handleBeginSession(Map<String, String> values) { 446 // ignore 447 } 448 449 /** 450 * Handles end of dEQP session. 451 */ handleEndSession(Map<String, String> values)452 private void handleEndSession(Map<String, String> values) { 453 // ignore 454 } 455 456 /** 457 * Handles beginning of dEQP testcase. 458 */ handleBeginTestCase(Map<String, String> values)459 private void handleBeginTestCase(Map<String, String> values) { 460 mCurrentTestId = pathToIdentifier(values.get("dEQP-BeginTestCase-TestCasePath")); 461 mCurrentTestLog = ""; 462 mGotTestResult = false; 463 464 // mark instance as started 465 if (mPendingResults.get(mCurrentTestId) != null) { 466 mPendingResults.get(mCurrentTestId).remainingConfigs.remove(mRunConfig); 467 } else { 468 CLog.w("Got unexpected start of %s", mCurrentTestId); 469 } 470 } 471 472 /** 473 * Handles end of dEQP testcase. 474 */ handleEndTestCase(Map<String, String> values)475 private void handleEndTestCase(Map<String, String> values) { 476 final PendingResult result = mPendingResults.get(mCurrentTestId); 477 478 if (result != null) { 479 if (!mGotTestResult) { 480 result.allInstancesPassed = false; 481 result.errorMessages.put(mRunConfig, INCOMPLETE_LOG_MESSAGE); 482 } 483 484 if (mLogData && mCurrentTestLog != null && mCurrentTestLog.length() > 0) { 485 result.testLogs.put(mRunConfig, mCurrentTestLog); 486 } 487 488 // Pending result finished, report result 489 if (result.remainingConfigs.isEmpty()) { 490 forwardFinalizedPendingResult(mCurrentTestId); 491 } 492 } else { 493 CLog.w("Got unexpected end of %s", mCurrentTestId); 494 } 495 mCurrentTestId = null; 496 } 497 498 /** 499 * Handles dEQP testcase result. 500 */ handleTestCaseResult(Map<String, String> values)501 private void handleTestCaseResult(Map<String, String> values) { 502 String code = values.get("dEQP-TestCaseResult-Code"); 503 String details = values.get("dEQP-TestCaseResult-Details"); 504 505 if (mPendingResults.get(mCurrentTestId) == null) { 506 CLog.w("Got unexpected result for %s", mCurrentTestId); 507 mGotTestResult = true; 508 return; 509 } 510 511 if (code.compareTo("Pass") == 0) { 512 mGotTestResult = true; 513 } else if (code.compareTo("NotSupported") == 0) { 514 mGotTestResult = true; 515 } else if (code.compareTo("QualityWarning") == 0) { 516 mGotTestResult = true; 517 } else if (code.compareTo("CompatibilityWarning") == 0) { 518 mGotTestResult = true; 519 } else if (code.compareTo("Fail") == 0 || code.compareTo("ResourceError") == 0 520 || code.compareTo("InternalError") == 0 || code.compareTo("Crash") == 0 521 || code.compareTo("Timeout") == 0) { 522 mPendingResults.get(mCurrentTestId).allInstancesPassed = false; 523 mPendingResults.get(mCurrentTestId) 524 .errorMessages.put(mRunConfig, code + ": " + details); 525 mGotTestResult = true; 526 } else { 527 String codeError = "Unknown result code: " + code; 528 mPendingResults.get(mCurrentTestId).allInstancesPassed = false; 529 mPendingResults.get(mCurrentTestId) 530 .errorMessages.put(mRunConfig, codeError + ": " + details); 531 mGotTestResult = true; 532 } 533 } 534 535 /** 536 * Handles terminated dEQP testcase. 537 */ handleTestCaseTerminate(Map<String, String> values)538 private void handleTestCaseTerminate(Map<String, String> values) { 539 final PendingResult result = mPendingResults.get(mCurrentTestId); 540 541 if (result != null) { 542 String reason = values.get("dEQP-TerminateTestCase-Reason"); 543 mPendingResults.get(mCurrentTestId).allInstancesPassed = false; 544 mPendingResults.get(mCurrentTestId) 545 .errorMessages.put(mRunConfig, "Terminated: " + reason); 546 547 // Pending result finished, report result 548 if (result.remainingConfigs.isEmpty()) { 549 forwardFinalizedPendingResult(mCurrentTestId); 550 } 551 } else { 552 CLog.w("Got unexpected termination of %s", mCurrentTestId); 553 } 554 555 mCurrentTestId = null; 556 mGotTestResult = true; 557 } 558 559 /** 560 * Handles dEQP testlog data. 561 */ handleTestLogData(Map<String, String> values)562 private void handleTestLogData(Map<String, String> values) { 563 mCurrentTestLog = mCurrentTestLog + values.get("dEQP-TestLogData-Log"); 564 } 565 566 /** 567 * Handles new instrumentation status message. 568 */ handleStatus(Map<String, String> values)569 public void handleStatus(Map<String, String> values) { 570 String eventType = values.get("dEQP-EventType"); 571 572 if (eventType == null) { 573 return; 574 } 575 576 if (eventType.compareTo("BeginSession") == 0) { 577 handleBeginSession(values); 578 } else if (eventType.compareTo("EndSession") == 0) { 579 handleEndSession(values); 580 } else if (eventType.compareTo("BeginTestCase") == 0) { 581 handleBeginTestCase(values); 582 } else if (eventType.compareTo("EndTestCase") == 0) { 583 handleEndTestCase(values); 584 } else if (eventType.compareTo("TestCaseResult") == 0) { 585 handleTestCaseResult(values); 586 } else if (eventType.compareTo("TerminateTestCase") == 0) { 587 handleTestCaseTerminate(values); 588 } else if (eventType.compareTo("TestLogData") == 0) { 589 handleTestLogData(values); 590 } 591 } 592 593 /** 594 * Signal listener that batch ended and forget incomplete results. 595 */ endBatch()596 public void endBatch() { 597 // end open test if when stream ends 598 if (mCurrentTestId != null) { 599 // Current instance was removed from remainingConfigs when case 600 // started. Mark current instance as pending. 601 if (mPendingResults.get(mCurrentTestId) != null) { 602 mPendingResults.get(mCurrentTestId).remainingConfigs.add(mRunConfig); 603 } else { 604 CLog.w("Got unexpected internal state of %s", mCurrentTestId); 605 } 606 } 607 mCurrentTestId = null; 608 } 609 } 610 611 /** 612 * dEQP instrumentation parser 613 */ 614 private static class InstrumentationParser extends MultiLineReceiver { 615 private TestInstanceResultListener mListener; 616 617 private Map<String, String> mValues; 618 private String mCurrentName; 619 private String mCurrentValue; 620 private int mResultCode; 621 private boolean mGotExitValue = false; 622 623 InstrumentationParser(TestInstanceResultListener listener)624 public InstrumentationParser(TestInstanceResultListener listener) { 625 mListener = listener; 626 } 627 628 /** 629 * {@inheritDoc} 630 */ 631 @Override processNewLines(String[] lines)632 public void processNewLines(String[] lines) { 633 for (String line : lines) { 634 if (mValues == null) mValues = new HashMap<String, String>(); 635 636 if (line.startsWith("INSTRUMENTATION_STATUS_CODE: ")) { 637 if (mCurrentName != null) { 638 mValues.put(mCurrentName, mCurrentValue); 639 640 mCurrentName = null; 641 mCurrentValue = null; 642 } 643 644 mListener.handleStatus(mValues); 645 mValues = null; 646 } else if (line.startsWith("INSTRUMENTATION_STATUS: dEQP-")) { 647 if (mCurrentName != null) { 648 mValues.put(mCurrentName, mCurrentValue); 649 650 mCurrentValue = null; 651 mCurrentName = null; 652 } 653 654 String prefix = "INSTRUMENTATION_STATUS: "; 655 int nameBegin = prefix.length(); 656 int nameEnd = line.indexOf('='); 657 int valueBegin = nameEnd + 1; 658 659 mCurrentName = line.substring(nameBegin, nameEnd); 660 mCurrentValue = line.substring(valueBegin); 661 } else if (line.startsWith("INSTRUMENTATION_CODE: ")) { 662 try { 663 mResultCode = Integer.parseInt(line.substring(22)); 664 mGotExitValue = true; 665 } catch (NumberFormatException ex) { 666 CLog.w("Instrumentation code format unexpected"); 667 } 668 } else if (mCurrentValue != null) { 669 mCurrentValue = mCurrentValue + line; 670 } 671 } 672 } 673 674 /** 675 * {@inheritDoc} 676 */ 677 @Override done()678 public void done() { 679 if (mCurrentName != null) { 680 mValues.put(mCurrentName, mCurrentValue); 681 682 mCurrentName = null; 683 mCurrentValue = null; 684 } 685 686 if (mValues != null) { 687 mListener.handleStatus(mValues); 688 mValues = null; 689 } 690 } 691 692 /** 693 * {@inheritDoc} 694 */ 695 @Override isCancelled()696 public boolean isCancelled() { 697 return false; 698 } 699 700 /** 701 * Returns whether target instrumentation exited normally. 702 */ wasSuccessful()703 public boolean wasSuccessful() { 704 return mGotExitValue; 705 } 706 707 /** 708 * Returns Instrumentation return code 709 */ getResultCode()710 public int getResultCode() { 711 return mResultCode; 712 } 713 } 714 715 /** 716 * dEQP platfom query instrumentation parser 717 */ 718 private static class PlatformQueryInstrumentationParser extends MultiLineReceiver { 719 private Map<String,String> mResultMap = new LinkedHashMap<>(); 720 private int mResultCode; 721 private boolean mGotExitValue = false; 722 723 /** 724 * {@inheritDoc} 725 */ 726 @Override processNewLines(String[] lines)727 public void processNewLines(String[] lines) { 728 for (String line : lines) { 729 if (line.startsWith("INSTRUMENTATION_RESULT: ")) { 730 final String parts[] = line.substring(24).split("=",2); 731 if (parts.length == 2) { 732 mResultMap.put(parts[0], parts[1]); 733 } else { 734 CLog.w("Instrumentation status format unexpected"); 735 } 736 } else if (line.startsWith("INSTRUMENTATION_CODE: ")) { 737 try { 738 mResultCode = Integer.parseInt(line.substring(22)); 739 mGotExitValue = true; 740 } catch (NumberFormatException ex) { 741 CLog.w("Instrumentation code format unexpected"); 742 } 743 } 744 } 745 } 746 747 /** 748 * {@inheritDoc} 749 */ 750 @Override isCancelled()751 public boolean isCancelled() { 752 return false; 753 } 754 755 /** 756 * Returns whether target instrumentation exited normally. 757 */ wasSuccessful()758 public boolean wasSuccessful() { 759 return mGotExitValue; 760 } 761 762 /** 763 * Returns Instrumentation return code 764 */ getResultCode()765 public int getResultCode() { 766 return mResultCode; 767 } 768 getResultMap()769 public Map<String,String> getResultMap() { 770 return mResultMap; 771 } 772 } 773 774 /** 775 * Interface for sleeping. 776 * 777 * Exposed for unit testing 778 */ 779 public static interface ISleepProvider { sleep(int milliseconds)780 public void sleep(int milliseconds); 781 } 782 783 private static class SleepProvider implements ISleepProvider { 784 @Override sleep(int milliseconds)785 public void sleep(int milliseconds) { 786 RunUtil.getDefault().sleep(milliseconds); 787 } 788 } 789 790 /** 791 * Interface for failure recovery. 792 * 793 * Exposed for unit testing 794 */ 795 public static interface IRecovery { 796 /** 797 * Sets the sleep provider IRecovery works on 798 */ setSleepProvider(ISleepProvider sleepProvider)799 public void setSleepProvider(ISleepProvider sleepProvider); 800 801 /** 802 * Sets the device IRecovery works on 803 */ setDevice(ITestDevice device)804 public void setDevice(ITestDevice device); 805 806 /** 807 * Informs Recovery that test execution has progressed since the last recovery 808 */ onExecutionProgressed()809 public void onExecutionProgressed(); 810 811 /** 812 * Tries to recover device after failed refused connection. 813 * 814 * @throws DeviceNotAvailableException if recovery did not succeed 815 */ recoverConnectionRefused()816 public void recoverConnectionRefused() throws DeviceNotAvailableException; 817 818 /** 819 * Tries to recover device after abnormal execution termination or link failure. 820 * 821 * @throws DeviceNotAvailableException if recovery did not succeed 822 */ recoverComLinkKilled()823 public void recoverComLinkKilled() throws DeviceNotAvailableException; 824 } 825 826 /** 827 * State machine for execution failure recovery. 828 * 829 * Exposed for unit testing 830 */ 831 public static class Recovery implements IRecovery { 832 private int RETRY_COOLDOWN_MS = 6000; // 6 seconds 833 private int PROCESS_KILL_WAIT_MS = 1000; // 1 second 834 835 private static enum MachineState { 836 WAIT, // recover by waiting 837 RECOVER, // recover by calling recover() 838 REBOOT, // recover by rebooting 839 FAIL, // cannot recover 840 } 841 842 private MachineState mState = MachineState.WAIT; 843 private ITestDevice mDevice; 844 private ISleepProvider mSleepProvider; 845 846 private static class ProcessKillFailureException extends Exception { 847 } 848 849 /** 850 * {@inheritDoc} 851 */ 852 @Override setSleepProvider(ISleepProvider sleepProvider)853 public void setSleepProvider(ISleepProvider sleepProvider) { 854 mSleepProvider = sleepProvider; 855 } 856 857 /** 858 * {@inheritDoc} 859 */ 860 @Override setDevice(ITestDevice device)861 public void setDevice(ITestDevice device) { 862 mDevice = device; 863 } 864 865 /** 866 * {@inheritDoc} 867 */ 868 @Override onExecutionProgressed()869 public void onExecutionProgressed() { 870 mState = MachineState.WAIT; 871 } 872 873 /** 874 * {@inheritDoc} 875 */ 876 @Override recoverConnectionRefused()877 public void recoverConnectionRefused() throws DeviceNotAvailableException { 878 switch (mState) { 879 case WAIT: // not a valid stratedy for connection refusal, fallthrough 880 case RECOVER: 881 // First failure, just try to recover 882 CLog.w("ADB connection failed, trying to recover"); 883 mState = MachineState.REBOOT; // the next step is to reboot 884 885 try { 886 recoverDevice(); 887 } catch (DeviceNotAvailableException ex) { 888 // chain forward 889 recoverConnectionRefused(); 890 } 891 break; 892 893 case REBOOT: 894 // Second failure in a row, try to reboot 895 CLog.w("ADB connection failed after recovery, rebooting device"); 896 mState = MachineState.FAIL; // the next step is to fail 897 898 try { 899 rebootDevice(); 900 } catch (DeviceNotAvailableException ex) { 901 // chain forward 902 recoverConnectionRefused(); 903 } 904 break; 905 906 case FAIL: 907 // Third failure in a row, just fail 908 CLog.w("Cannot recover ADB connection"); 909 throw new DeviceNotAvailableException("failed to connect after reboot", 910 mDevice.getSerialNumber()); 911 } 912 } 913 914 /** 915 * {@inheritDoc} 916 */ 917 @Override recoverComLinkKilled()918 public void recoverComLinkKilled() throws DeviceNotAvailableException { 919 switch (mState) { 920 case WAIT: 921 // First failure, just try to wait and try again 922 CLog.w("ADB link failed, retrying after a cooldown period"); 923 mState = MachineState.RECOVER; // the next step is to recover the device 924 925 waitCooldown(); 926 927 // even if the link to deqp on-device process was killed, the process might 928 // still be alive. Locate and terminate such unwanted processes. 929 try { 930 killDeqpProcess(); 931 } catch (DeviceNotAvailableException ex) { 932 // chain forward 933 recoverComLinkKilled(); 934 } catch (ProcessKillFailureException ex) { 935 // chain forward 936 recoverComLinkKilled(); 937 } 938 break; 939 940 case RECOVER: 941 // Second failure, just try to recover 942 CLog.w("ADB link failed, trying to recover"); 943 mState = MachineState.REBOOT; // the next step is to reboot 944 945 try { 946 recoverDevice(); 947 killDeqpProcess(); 948 } catch (DeviceNotAvailableException ex) { 949 // chain forward 950 recoverComLinkKilled(); 951 } catch (ProcessKillFailureException ex) { 952 // chain forward 953 recoverComLinkKilled(); 954 } 955 break; 956 957 case REBOOT: 958 // Third failure in a row, try to reboot 959 CLog.w("ADB link failed after recovery, rebooting device"); 960 mState = MachineState.FAIL; // the next step is to fail 961 962 try { 963 rebootDevice(); 964 } catch (DeviceNotAvailableException ex) { 965 // chain forward 966 recoverComLinkKilled(); 967 } 968 break; 969 970 case FAIL: 971 // Fourth failure in a row, just fail 972 CLog.w("Cannot recover ADB connection"); 973 throw new DeviceNotAvailableException("link killed after reboot", 974 mDevice.getSerialNumber()); 975 } 976 } 977 waitCooldown()978 private void waitCooldown() { 979 mSleepProvider.sleep(RETRY_COOLDOWN_MS); 980 } 981 getDeqpProcessPids()982 private Iterable<Integer> getDeqpProcessPids() throws DeviceNotAvailableException { 983 final List<Integer> pids = new ArrayList<Integer>(2); 984 final String processes = mDevice.executeShellCommand("ps | grep com.drawelements"); 985 final String[] lines = processes.split("(\\r|\\n)+"); 986 for (String line : lines) { 987 final String[] fields = line.split("\\s+"); 988 if (fields.length < 2) { 989 continue; 990 } 991 992 try { 993 final int processId = Integer.parseInt(fields[1], 10); 994 pids.add(processId); 995 } catch (NumberFormatException ex) { 996 continue; 997 } 998 } 999 return pids; 1000 } 1001 killDeqpProcess()1002 private void killDeqpProcess() throws DeviceNotAvailableException, 1003 ProcessKillFailureException { 1004 for (Integer processId : getDeqpProcessPids()) { 1005 mDevice.executeShellCommand(String.format("kill -9 %d", processId)); 1006 } 1007 1008 mSleepProvider.sleep(PROCESS_KILL_WAIT_MS); 1009 1010 // check that processes actually died 1011 if (getDeqpProcessPids().iterator().hasNext()) { 1012 // a process is still alive, killing failed 1013 throw new ProcessKillFailureException(); 1014 } 1015 } 1016 recoverDevice()1017 public void recoverDevice() throws DeviceNotAvailableException { 1018 ((IManagedTestDevice) mDevice).recoverDevice(); 1019 } 1020 rebootDevice()1021 private void rebootDevice() throws DeviceNotAvailableException { 1022 mDevice.reboot(); 1023 } 1024 } 1025 1026 addTestsToInstancesMap( File testlist, String configName, String screenRotation, String surfaceType, boolean required, Map<TestDescription, Set<BatchRunConfiguration>> instances)1027 private static void addTestsToInstancesMap( 1028 File testlist, 1029 String configName, 1030 String screenRotation, 1031 String surfaceType, 1032 boolean required, 1033 Map<TestDescription, Set<BatchRunConfiguration>> instances) { 1034 1035 try (final FileReader testlistInnerReader = new FileReader(testlist); 1036 final BufferedReader testlistReader = new BufferedReader(testlistInnerReader)) { 1037 1038 String testName; 1039 while ((testName = testlistReader.readLine()) != null) { 1040 testName = testName.trim(); 1041 1042 // Skip empty lines. 1043 if (testName.isEmpty()) { 1044 continue; 1045 } 1046 1047 // Lines starting with "#" are comments. 1048 if (testName.startsWith("#")) { 1049 continue; 1050 } 1051 1052 // If the "testName" ends with .txt, then it is a path to another test list 1053 // (relative to the current test list, path separator is "/") that we need to 1054 // read. 1055 if (testName.endsWith(".txt")) { 1056 addTestsToInstancesMap( 1057 Paths.get(testlist.getParent(), testName.split("/")).toFile(), 1058 configName, 1059 screenRotation, 1060 surfaceType, 1061 required, 1062 instances); 1063 continue; 1064 } 1065 1066 // Test name -> testId -> only one config -> done. 1067 final Set<BatchRunConfiguration> testInstanceSet = new LinkedHashSet<>(); 1068 BatchRunConfiguration config = new BatchRunConfiguration(configName, screenRotation, surfaceType, required); 1069 testInstanceSet.add(config); 1070 TestDescription test = pathToIdentifier(testName); 1071 instances.put(test, testInstanceSet); 1072 } 1073 } 1074 catch (IOException e) 1075 { 1076 throw new RuntimeException("Failure while reading the test case list for deqp: " + e.getMessage()); 1077 } 1078 } 1079 getTestRunConfigs(TestDescription testId)1080 private Set<BatchRunConfiguration> getTestRunConfigs(TestDescription testId) { 1081 return mTestInstances.get(testId); 1082 } 1083 1084 /** 1085 * Get the test instance of the runner. Exposed for testing. 1086 */ getTestInstance()1087 Map<TestDescription, Set<BatchRunConfiguration>> getTestInstance() { 1088 return mTestInstances; 1089 } 1090 1091 /** 1092 * Converts dEQP testcase path to TestDescription. 1093 */ pathToIdentifier(String testPath)1094 private static TestDescription pathToIdentifier(String testPath) { 1095 int indexOfLastDot = testPath.lastIndexOf('.'); 1096 String className = testPath.substring(0, indexOfLastDot); 1097 String testName = testPath.substring(indexOfLastDot+1); 1098 1099 return new TestDescription(className, testName); 1100 } 1101 1102 // \todo [2015-10-16 kalle] How unique should this be? getId()1103 private String getId() { 1104 return AbiUtils.createId(mAbi.getName(), mDeqpPackage); 1105 } 1106 1107 /** 1108 * Generates tescase trie from dEQP testcase paths. Used to define which testcases to execute. 1109 */ generateTestCaseTrieFromPaths(Collection<String> tests)1110 private static String generateTestCaseTrieFromPaths(Collection<String> tests) { 1111 String result = "{"; 1112 boolean first = true; 1113 1114 // Add testcases to results 1115 for (Iterator<String> iter = tests.iterator(); iter.hasNext();) { 1116 String test = iter.next(); 1117 String[] components = test.split("\\."); 1118 1119 if (components.length == 1) { 1120 if (!first) { 1121 result = result + ","; 1122 } 1123 first = false; 1124 1125 result += components[0]; 1126 iter.remove(); 1127 } 1128 } 1129 1130 if (!tests.isEmpty()) { 1131 HashMap<String, ArrayList<String> > testGroups = new HashMap<>(); 1132 1133 // Collect all sub testgroups 1134 for (String test : tests) { 1135 String[] components = test.split("\\."); 1136 ArrayList<String> testGroup = testGroups.get(components[0]); 1137 1138 if (testGroup == null) { 1139 testGroup = new ArrayList<String>(); 1140 testGroups.put(components[0], testGroup); 1141 } 1142 1143 testGroup.add(test.substring(components[0].length()+1)); 1144 } 1145 1146 for (String testGroup : testGroups.keySet()) { 1147 if (!first) { 1148 result = result + ","; 1149 } 1150 1151 first = false; 1152 result = result + testGroup 1153 + generateTestCaseTrieFromPaths(testGroups.get(testGroup)); 1154 } 1155 } 1156 1157 return result + "}"; 1158 } 1159 1160 /** 1161 * Generates testcase trie from TestDescriptions. 1162 */ generateTestCaseTrie(Collection<TestDescription> tests)1163 private static String generateTestCaseTrie(Collection<TestDescription> tests) { 1164 ArrayList<String> testPaths = new ArrayList<String>(); 1165 1166 for (TestDescription test : tests) { 1167 testPaths.add(test.getClassName() + "." + test.getTestName()); 1168 } 1169 1170 return generateTestCaseTrieFromPaths(testPaths); 1171 } 1172 1173 private static class TestBatch { 1174 public BatchRunConfiguration config; 1175 public List<TestDescription> tests; 1176 } 1177 1178 /** 1179 * Creates a TestBatch from the given tests or null if not tests remaining. 1180 * 1181 * @param pool List of tests to select from 1182 * @param requiredConfig Select only instances with pending requiredConfig, or null to select 1183 * any run configuration. 1184 */ selectRunBatch(Collection<TestDescription> pool, BatchRunConfiguration requiredConfig)1185 private TestBatch selectRunBatch(Collection<TestDescription> pool, 1186 BatchRunConfiguration requiredConfig) { 1187 // select one test (leading test) that is going to be executed and then pack along as many 1188 // other compatible instances as possible. 1189 1190 TestDescription leadingTest = null; 1191 for (TestDescription test : pool) { 1192 if (!mRemainingTests.contains(test)) { 1193 continue; 1194 } 1195 if (requiredConfig != null && 1196 !mInstanceListerner.isPendingTestInstance(test, requiredConfig)) { 1197 continue; 1198 } 1199 leadingTest = test; 1200 break; 1201 } 1202 1203 // no remaining tests? 1204 if (leadingTest == null) { 1205 return null; 1206 } 1207 1208 BatchRunConfiguration leadingTestConfig = null; 1209 if (requiredConfig != null) { 1210 leadingTestConfig = requiredConfig; 1211 } else { 1212 for (BatchRunConfiguration runConfig : getTestRunConfigs(leadingTest)) { 1213 if (mInstanceListerner.isPendingTestInstance(leadingTest, runConfig)) { 1214 leadingTestConfig = runConfig; 1215 break; 1216 } 1217 } 1218 } 1219 1220 // test pending <=> test has a pending config 1221 if (leadingTestConfig == null) { 1222 throw new AssertionError("search postcondition failed"); 1223 } 1224 1225 final int leadingInstability = getTestInstabilityRating(leadingTest); 1226 1227 final TestBatch runBatch = new TestBatch(); 1228 runBatch.config = leadingTestConfig; 1229 runBatch.tests = new ArrayList<>(); 1230 runBatch.tests.add(leadingTest); 1231 1232 for (TestDescription test : pool) { 1233 if (test == leadingTest) { 1234 // do not re-select the leading tests 1235 continue; 1236 } 1237 if (!mInstanceListerner.isPendingTestInstance(test, leadingTestConfig)) { 1238 // select only compatible 1239 continue; 1240 } 1241 if (getTestInstabilityRating(test) != leadingInstability) { 1242 // pack along only cases in the same stability category. Packing more dangerous 1243 // tests along jeopardizes the stability of this run. Packing more stable tests 1244 // along jeopardizes their stability rating. 1245 continue; 1246 } 1247 if (runBatch.tests.size() >= getBatchSizeLimitForInstability(leadingInstability)) { 1248 // batch size is limited. 1249 break; 1250 } 1251 runBatch.tests.add(test); 1252 } 1253 1254 return runBatch; 1255 } 1256 getBatchSizeLimit()1257 private int getBatchSizeLimit() { 1258 if (isIncrementalDeqpRun()) { 1259 return TESTCASE_BATCH_LIMIT_LARGE; 1260 } 1261 return TESTCASE_BATCH_LIMIT; 1262 } 1263 getBatchNumPendingCases(TestBatch batch)1264 private int getBatchNumPendingCases(TestBatch batch) { 1265 int numPending = 0; 1266 for (TestDescription test : batch.tests) { 1267 if (mInstanceListerner.isPendingTestInstance(test, batch.config)) { 1268 ++numPending; 1269 } 1270 } 1271 return numPending; 1272 } 1273 getBatchSizeLimitForInstability(int batchInstabilityRating)1274 private int getBatchSizeLimitForInstability(int batchInstabilityRating) { 1275 // reduce group size exponentially down to one 1276 return Math.max(1, getBatchSizeLimit() / (1 << batchInstabilityRating)); 1277 } 1278 getTestInstabilityRating(TestDescription testId)1279 private int getTestInstabilityRating(TestDescription testId) { 1280 if (mTestInstabilityRatings.containsKey(testId)) { 1281 return mTestInstabilityRatings.get(testId); 1282 } else { 1283 return 0; 1284 } 1285 } 1286 recordTestInstability(TestDescription testId)1287 private void recordTestInstability(TestDescription testId) { 1288 mTestInstabilityRatings.put(testId, getTestInstabilityRating(testId) + 1); 1289 } 1290 clearTestInstability(TestDescription testId)1291 private void clearTestInstability(TestDescription testId) { 1292 mTestInstabilityRatings.put(testId, 0); 1293 } 1294 1295 /** 1296 * Executes all tests on the device. 1297 */ runTests()1298 private void runTests() throws DeviceNotAvailableException, CapabilityQueryFailureException { 1299 for (;;) { 1300 TestBatch batch = selectRunBatch(mTestInstances.keySet(), null); 1301 1302 if (batch == null) { 1303 break; 1304 } 1305 1306 runTestRunBatch(batch); 1307 } 1308 } 1309 1310 /** 1311 * Runs a TestBatch by either faking it or executing it on a device. 1312 */ runTestRunBatch(TestBatch batch)1313 private void runTestRunBatch(TestBatch batch) throws DeviceNotAvailableException, 1314 CapabilityQueryFailureException { 1315 // prepare instance listener 1316 mInstanceListerner.setCurrentConfig(batch.config); 1317 for (TestDescription test : batch.tests) { 1318 mInstanceListerner.setTestInstances(test, getTestRunConfigs(test)); 1319 } 1320 1321 // When incremental dEQP is enabled, skip all tests except those in 1322 // mIncrementalDeqpIncludeTests 1323 if (isIncrementalDeqpRun()) { 1324 TestBatch skipBatch = new TestBatch(); 1325 skipBatch.config = batch.config; 1326 skipBatch.tests = new ArrayList<>(); 1327 TestBatch runBatch = new TestBatch(); 1328 runBatch.config = batch.config; 1329 runBatch.tests = new ArrayList<>(); 1330 for (TestDescription test : batch.tests) { 1331 if (mIncrementalDeqpIncludeTests.contains(test.getClassName() + "." 1332 + test.getTestName())) { 1333 runBatch.tests.add(test); 1334 } else { 1335 skipBatch.tests.add(test); 1336 } 1337 } 1338 batch = runBatch; 1339 fakePassTestRunBatch(skipBatch); 1340 if (batch.tests.isEmpty()) { 1341 return; 1342 } 1343 } 1344 // execute only if config is executable, else fake results 1345 if (isSupportedRunConfiguration(batch.config)) { 1346 executeTestRunBatch(batch); 1347 } else { 1348 if (batch.config.isRequired()) { 1349 fakeFailTestRunBatch(batch); 1350 } else { 1351 fakePassTestRunBatch(batch); 1352 } 1353 } 1354 } 1355 isIncrementalDeqpRun()1356 private boolean isIncrementalDeqpRun() { 1357 IBuildInfo buildInfo = mBuildHelper.getBuildInfo(); 1358 return buildInfo.getBuildAttributes().containsKey( 1359 IncrementalDeqpPreparer.INCREMENTAL_DEQP_ATTRIBUTE_NAME); 1360 } 1361 isSupportedRunConfiguration(BatchRunConfiguration runConfig)1362 private boolean isSupportedRunConfiguration(BatchRunConfiguration runConfig) 1363 throws DeviceNotAvailableException, CapabilityQueryFailureException { 1364 // orientation support 1365 if (!BatchRunConfiguration.ROTATION_UNSPECIFIED.equals(runConfig.getRotation())) { 1366 final Map<String, Optional<Integer>> features = getDeviceFeatures(mDevice); 1367 1368 if (isPortraitClassRotation(runConfig.getRotation()) && 1369 !features.containsKey(FEATURE_PORTRAIT)) { 1370 return false; 1371 } 1372 if (isLandscapeClassRotation(runConfig.getRotation()) && 1373 !features.containsKey(FEATURE_LANDSCAPE)) { 1374 return false; 1375 } 1376 } 1377 1378 if (isOpenGlEsPackage()) { 1379 // renderability support for OpenGL ES tests 1380 return isSupportedGlesRenderConfig(runConfig); 1381 } else { 1382 return true; 1383 } 1384 } 1385 1386 private static final class AdbComLinkOpenError extends Exception { AdbComLinkOpenError(String description, Throwable inner)1387 public AdbComLinkOpenError(String description, Throwable inner) { 1388 super(description, inner); 1389 } 1390 } 1391 1392 private static final class AdbComLinkKilledError extends Exception { AdbComLinkKilledError(String description, Throwable inner)1393 public AdbComLinkKilledError(String description, Throwable inner) { 1394 super(description, inner); 1395 } 1396 } 1397 1398 private static final class AdbComLinkUnresponsiveError extends Exception { AdbComLinkUnresponsiveError(String description, Throwable inner)1399 public AdbComLinkUnresponsiveError(String description, Throwable inner) { 1400 super(description, inner); 1401 } 1402 } 1403 1404 /** 1405 * Executes a given command in adb shell 1406 * 1407 * @throws AdbComLinkOpenError if connection cannot be established. 1408 * @throws AdbComLinkKilledError if established connection is killed prematurely. 1409 */ executeShellCommandAndReadOutput(final String command, final IShellOutputReceiver receiver)1410 private void executeShellCommandAndReadOutput(final String command, 1411 final IShellOutputReceiver receiver) 1412 throws AdbComLinkOpenError, AdbComLinkKilledError, AdbComLinkUnresponsiveError { 1413 try { 1414 mDevice.getIDevice().executeShellCommand(command, receiver, 1415 mUnresponsiveCmdTimeoutMs, TimeUnit.MILLISECONDS); 1416 } catch (TimeoutException ex) { 1417 // Opening connection timed out 1418 throw new AdbComLinkOpenError("opening connection timed out", ex); 1419 } catch (AdbCommandRejectedException ex) { 1420 // Command rejected 1421 throw new AdbComLinkOpenError("command rejected", ex); 1422 } catch (IOException ex) { 1423 // shell command channel killed 1424 throw new AdbComLinkKilledError("command link killed", ex); 1425 } catch (ShellCommandUnresponsiveException ex) { 1426 // shell command halted 1427 throw new AdbComLinkUnresponsiveError("command link was unresponsive for longer than requested timeout", ex); 1428 } 1429 } 1430 1431 /** 1432 * Executes given test batch on a device 1433 */ executeTestRunBatch(TestBatch batch)1434 private void executeTestRunBatch(TestBatch batch) throws DeviceNotAvailableException { 1435 // attempt full run once 1436 executeTestRunBatchRun(batch); 1437 1438 // split remaining tests to two sub batches and execute both. This will terminate 1439 // since executeTestRunBatchRun will always progress for a batch of size 1. 1440 final ArrayList<TestDescription> pendingTests = new ArrayList<>(); 1441 1442 for (TestDescription test : batch.tests) { 1443 if (mInstanceListerner.isPendingTestInstance(test, batch.config)) { 1444 pendingTests.add(test); 1445 } 1446 } 1447 1448 final int divisorNdx = pendingTests.size() / 2; 1449 final List<TestDescription> headList = pendingTests.subList(0, divisorNdx); 1450 final List<TestDescription> tailList = pendingTests.subList(divisorNdx, pendingTests.size()); 1451 1452 // head 1453 for (;;) { 1454 TestBatch subBatch = selectRunBatch(headList, batch.config); 1455 1456 if (subBatch == null) { 1457 break; 1458 } 1459 1460 executeTestRunBatch(subBatch); 1461 } 1462 1463 // tail 1464 for (;;) { 1465 TestBatch subBatch = selectRunBatch(tailList, batch.config); 1466 1467 if (subBatch == null) { 1468 break; 1469 } 1470 1471 executeTestRunBatch(subBatch); 1472 } 1473 1474 if (getBatchNumPendingCases(batch) != 0) { 1475 throw new AssertionError("executeTestRunBatch postcondition failed"); 1476 } 1477 } 1478 1479 /** 1480 * Runs one execution pass over the given batch. 1481 * 1482 * Tries to run the batch. Always makes progress (executes instances or modifies stability 1483 * scores). 1484 */ executeTestRunBatchRun(TestBatch batch)1485 private void executeTestRunBatchRun(TestBatch batch) throws DeviceNotAvailableException { 1486 if (getBatchNumPendingCases(batch) != batch.tests.size()) { 1487 throw new AssertionError("executeTestRunBatchRun precondition failed"); 1488 } 1489 1490 checkInterrupted(); // throws if interrupted 1491 1492 final String testCases = generateTestCaseTrie(batch.tests); 1493 final String testCaseFilename = APP_DIR + CASE_LIST_FILE_NAME; 1494 mDevice.executeShellCommand("rm " + testCaseFilename); 1495 mDevice.executeShellCommand("rm " + APP_DIR + LOG_FILE_NAME); 1496 if (!mDevice.pushString(testCases + "\n", testCaseFilename)) { 1497 throw new RuntimeException("Failed to write test cases to " + testCaseFilename); 1498 } 1499 1500 final String instrumentationName = 1501 "com.drawelements.deqp/com.drawelements.deqp.testercore.DeqpInstrumentation"; 1502 1503 final StringBuilder deqpCmdLine = new StringBuilder(); 1504 deqpCmdLine.append("--deqp-caselist-file="); 1505 deqpCmdLine.append(APP_DIR + CASE_LIST_FILE_NAME); 1506 deqpCmdLine.append(" "); 1507 deqpCmdLine.append(getRunConfigDisplayCmdLine(batch.config)); 1508 1509 // If we are not logging data, do not bother outputting the images from the test exe. 1510 if (!mLogData) { 1511 deqpCmdLine.append(" --deqp-log-images=disable"); 1512 } 1513 1514 if (!mDisableWatchdog) { 1515 deqpCmdLine.append(" --deqp-watchdog=enable"); 1516 } 1517 1518 final String command = String.format( 1519 "am instrument %s -w -e deqpLogFilename \"%s\" -e deqpCmdLine \"%s\"" 1520 + " -e deqpLogData \"%s\" %s", 1521 AbiUtils.createAbiFlag(mAbi.getName()), APP_DIR + LOG_FILE_NAME, 1522 deqpCmdLine.toString(), mLogData, instrumentationName); 1523 1524 final int numRemainingInstancesBefore = getNumRemainingInstances(); 1525 final InstrumentationParser parser = new InstrumentationParser(mInstanceListerner); 1526 Throwable interruptingError = null; 1527 1528 //Fix the requirement of sleep() between batches 1529 long duration = System.currentTimeMillis() - mTimeOfLastRun; 1530 if (duration < 5000) { 1531 CLog.i("Sleeping for %dms", 5000 - duration); 1532 mRunUtil.sleep(5000 - duration); 1533 } 1534 1535 try { 1536 executeShellCommandAndReadOutput(command, parser); 1537 } catch (Throwable ex) { 1538 interruptingError = ex; 1539 } finally { 1540 mTimeOfLastRun = System.currentTimeMillis(); 1541 parser.flush(); 1542 } 1543 1544 final boolean progressedSinceLastCall = mInstanceListerner.getCurrentTestId() != null || 1545 getNumRemainingInstances() < numRemainingInstancesBefore; 1546 1547 if (progressedSinceLastCall) { 1548 mDeviceRecovery.onExecutionProgressed(); 1549 } 1550 1551 // interrupted either because of ADB or test timeout 1552 if (interruptingError != null) { 1553 1554 // AdbComLinkUnresponsiveError means the test has timeout during execution. 1555 // Device is likely fine, so we won't attempt to recover the device. 1556 if (interruptingError instanceof AdbComLinkUnresponsiveError) { 1557 mInstanceListerner.abortTest(mInstanceListerner.getCurrentTestId(), TIMEOUT_LOG_MESSAGE); 1558 } else if (interruptingError instanceof AdbComLinkOpenError) { 1559 mDeviceRecovery.recoverConnectionRefused(); 1560 } else if (interruptingError instanceof AdbComLinkKilledError) { 1561 mDeviceRecovery.recoverComLinkKilled(); 1562 } else if (interruptingError instanceof RunInterruptedException) { 1563 // external run interruption request. Terminate immediately. 1564 throw (RunInterruptedException)interruptingError; 1565 } else { 1566 CLog.e(interruptingError); 1567 throw new RuntimeException(interruptingError); 1568 } 1569 1570 // recoverXXX did not throw => recovery succeeded 1571 } else if (!parser.wasSuccessful()) { 1572 mDeviceRecovery.recoverComLinkKilled(); 1573 // recoverXXX did not throw => recovery succeeded 1574 } 1575 1576 // Progress guarantees. 1577 if (batch.tests.size() == 1) { 1578 final TestDescription onlyTest = batch.tests.iterator().next(); 1579 final boolean wasTestExecuted = 1580 !mInstanceListerner.isPendingTestInstance(onlyTest, batch.config) && 1581 mInstanceListerner.getCurrentTestId() == null; 1582 final boolean wasLinkFailure = !parser.wasSuccessful() || interruptingError != null; 1583 1584 // Link failures can be caused by external events, require at least two observations 1585 // until bailing. 1586 if (!wasTestExecuted && (!wasLinkFailure || getTestInstabilityRating(onlyTest) > 0)) { 1587 recordTestInstability(onlyTest); 1588 // If we cannot finish the test, mark the case as a crash. 1589 // 1590 // If we couldn't even start the test, fail the test instance as non-executable. 1591 // This is required so that a consistently crashing or non-existent tests will 1592 // not cause futile (non-terminating) re-execution attempts. 1593 if (mInstanceListerner.getCurrentTestId() != null) { 1594 mInstanceListerner.abortTest(onlyTest, INCOMPLETE_LOG_MESSAGE); 1595 } else { 1596 mInstanceListerner.abortTest(onlyTest, NOT_EXECUTABLE_LOG_MESSAGE); 1597 } 1598 } else if (wasTestExecuted) { 1599 clearTestInstability(onlyTest); 1600 } 1601 } 1602 else 1603 { 1604 // Analyze results to update test stability ratings. If there is no interrupting test 1605 // logged, increase instability rating of all remaining tests. If there is a 1606 // interrupting test logged, increase only its instability rating. 1607 // 1608 // A successful run of tests clears instability rating. 1609 if (mInstanceListerner.getCurrentTestId() == null) { 1610 for (TestDescription test : batch.tests) { 1611 if (mInstanceListerner.isPendingTestInstance(test, batch.config)) { 1612 recordTestInstability(test); 1613 } else { 1614 clearTestInstability(test); 1615 } 1616 } 1617 } else { 1618 recordTestInstability(mInstanceListerner.getCurrentTestId()); 1619 for (TestDescription test : batch.tests) { 1620 // \note: isPendingTestInstance is false for getCurrentTestId. Current ID is 1621 // considered 'running' and will be restored to 'pending' in endBatch(). 1622 if (!test.equals(mInstanceListerner.getCurrentTestId()) && 1623 !mInstanceListerner.isPendingTestInstance(test, batch.config)) { 1624 clearTestInstability(test); 1625 } 1626 } 1627 } 1628 } 1629 1630 mInstanceListerner.endBatch(); 1631 } 1632 getRunConfigDisplayCmdLine(BatchRunConfiguration runConfig)1633 private static String getRunConfigDisplayCmdLine(BatchRunConfiguration runConfig) { 1634 final StringBuilder deqpCmdLine = new StringBuilder(); 1635 if (!runConfig.getGlConfig().isEmpty()) { 1636 deqpCmdLine.append("--deqp-gl-config-name="); 1637 deqpCmdLine.append(runConfig.getGlConfig()); 1638 } 1639 if (!runConfig.getRotation().isEmpty()) { 1640 if (deqpCmdLine.length() != 0) { 1641 deqpCmdLine.append(" "); 1642 } 1643 deqpCmdLine.append("--deqp-screen-rotation="); 1644 deqpCmdLine.append(runConfig.getRotation()); 1645 } 1646 if (!runConfig.getSurfaceType().isEmpty()) { 1647 if (deqpCmdLine.length() != 0) { 1648 deqpCmdLine.append(" "); 1649 } 1650 deqpCmdLine.append("--deqp-surface-type="); 1651 deqpCmdLine.append(runConfig.getSurfaceType()); 1652 } 1653 return deqpCmdLine.toString(); 1654 } 1655 getNumRemainingInstances()1656 private int getNumRemainingInstances() { 1657 int retVal = 0; 1658 for (TestDescription testId : mRemainingTests) { 1659 // If case is in current working set, sum only not yet executed instances. 1660 // If case is not in current working set, sum all instances (since they are not yet 1661 // executed). 1662 if (mInstanceListerner.mPendingResults.containsKey(testId)) { 1663 retVal += mInstanceListerner.mPendingResults.get(testId).remainingConfigs.size(); 1664 } else { 1665 retVal += mTestInstances.get(testId).size(); 1666 } 1667 } 1668 return retVal; 1669 } 1670 1671 /** 1672 * Checks if this execution has been marked as interrupted and throws if it has. 1673 */ checkInterrupted()1674 private void checkInterrupted() throws RunInterruptedException { 1675 // Work around the API. RunUtil::checkInterrupted is private but we can call it indirectly 1676 // by sleeping a value <= 0. 1677 mRunUtil.sleep(0); 1678 } 1679 1680 /** 1681 * Pass given batch tests without running it 1682 */ fakePassTestRunBatch(TestBatch batch)1683 private void fakePassTestRunBatch(TestBatch batch) { 1684 for (TestDescription test : batch.tests) { 1685 CLog.d("Marking '%s' invocation in config '%s' as passed without running", test.toString(), 1686 batch.config.getId()); 1687 mInstanceListerner.skipTest(test); 1688 } 1689 } 1690 1691 /** 1692 * Fail given batch tests without running it 1693 */ fakeFailTestRunBatch(TestBatch batch)1694 private void fakeFailTestRunBatch(TestBatch batch) { 1695 for (TestDescription test : batch.tests) { 1696 CLog.d("Marking '%s' invocation in config '%s' as failed without running", test.toString(), 1697 batch.config.getId()); 1698 mInstanceListerner.abortTest(test, "Required config not supported"); 1699 } 1700 } 1701 1702 /** 1703 * Pass all remaining tests without running them 1704 */ fakePassTests(ITestInvocationListener listener)1705 private void fakePassTests(ITestInvocationListener listener) { 1706 HashMap<String, Metric> emptyMap = new HashMap<>(); 1707 for (TestDescription test : mTestInstances.keySet()) { 1708 listener.testStarted(test); 1709 listener.testEnded(test, emptyMap); 1710 } 1711 // Log only once all the skipped tests 1712 CLog.d("Skipping tests '%s', either because they are not supported by the device or " 1713 + "because tests are simply being collected", mRemainingTests); 1714 mRemainingTests.clear(); 1715 } 1716 1717 /** 1718 * Check if device supports Vulkan. 1719 */ isSupportedVulkan()1720 private boolean isSupportedVulkan () 1721 throws DeviceNotAvailableException, CapabilityQueryFailureException { 1722 final Map<String, Optional<Integer>> features = getDeviceFeatures(mDevice); 1723 1724 for (String feature : features.keySet()) { 1725 if (feature.startsWith(FEATURE_VULKAN_LEVEL)) { 1726 return true; 1727 } 1728 } 1729 1730 return false; 1731 } 1732 1733 /** 1734 * Check whether the device's claimed dEQP level is high enough that it should 1735 * pass the tests in the caselist. 1736 */ claimedDeqpLevelIsRecentEnough()1737 private boolean claimedDeqpLevelIsRecentEnough() throws CapabilityQueryFailureException, 1738 DeviceNotAvailableException { 1739 // Determine whether we need to check the dEQP feature flag for Vulkan or OpenGL ES. 1740 final String featureName; 1741 if (isVulkanPackage()) { 1742 featureName = FEATURE_VULKAN_DEQP_LEVEL; 1743 } else if (isOpenGlEsPackage() || isEglPackage()) { 1744 // The OpenGL ES feature flag is used for EGL as well. 1745 featureName = FEATURE_OPENGLES_DEQP_LEVEL; 1746 } else { 1747 throw new AssertionError( 1748 "Claims about dEQP support should only be checked for Vulkan, OpenGL ES, or EGL " 1749 + "packages"); 1750 } 1751 1752 CLog.d("For caselist \"%s\", the dEQP level feature flag is \"%s\".", mCaselistFile, 1753 featureName); 1754 1755 // A Vulkan/OpenGL ES caselist filename has the form: 1756 // {gles2,gles3,gles31,vk,egl}-master-YYYY-MM-DD.txt 1757 final Pattern caseListFilenamePattern = Pattern 1758 .compile("-master-(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)\\.txt$"); 1759 final Matcher matcher = caseListFilenamePattern.matcher(mCaselistFile); 1760 if (!matcher.find()) { 1761 CLog.d("No dEQP level date found in caselist. Running unconditionally."); 1762 return true; 1763 } 1764 final int year = Integer.parseInt(matcher.group(1)); 1765 final int month = Integer.parseInt(matcher.group(2)); 1766 final int day = Integer.parseInt(matcher.group(3)); 1767 CLog.d("Caselist date is %04d-%02d-%02d", year, month, day); 1768 1769 // As per the documentation for FEATURE_VULKAN_DEQP_LEVEL and 1770 // FEATURE_OPENGLES_DEQP_LEVEL in android.content.pm.PackageManager, a year is 1771 // encoded as an integer by devoting bits 31-16 to year, 15-8 to month and 7-0 1772 // to day. 1773 final int minimumLevel = (year << 16) + (month << 8) + day; 1774 1775 CLog.d("For reference, date -> level mappings are:"); 1776 CLog.d(" 2019-03-01 -> 132317953"); 1777 CLog.d(" 2020-03-01 -> 132383489"); 1778 CLog.d(" 2021-03-01 -> 132449025"); 1779 CLog.d(" 2022-03-01 -> 132514561"); 1780 1781 CLog.d("Minimum level required to run this caselist is %d", minimumLevel); 1782 1783 // Now look for the feature flag. 1784 final Map<String, Optional<Integer>> features = getDeviceFeatures(mDevice); 1785 1786 for (String feature : features.keySet()) { 1787 if (feature.startsWith(featureName)) { 1788 final Optional<Integer> claimedDeqpLevel = features.get(feature); 1789 if (!claimedDeqpLevel.isPresent()) { 1790 throw new IllegalStateException("Feature " + featureName 1791 + " has no associated version"); 1792 } 1793 CLog.d("Device level is %d", claimedDeqpLevel.get()); 1794 1795 final boolean shouldRunCaselist = claimedDeqpLevel.get() >= minimumLevel; 1796 CLog.d("Running caselist? %b", shouldRunCaselist); 1797 return shouldRunCaselist; 1798 } 1799 } 1800 1801 CLog.d("Could not find dEQP level feature flag \"%s\".", 1802 featureName); 1803 1804 // A Vulkan dEQP level has been required since R. 1805 // A GLES/EGL dEQP level has only been required since S. 1806 // Thus, if the VSR level is <= R and there is no GLES dEQP level, then we can assume 1807 // a GLES dEQP level of R (2020). 1808 if (PropertyUtil.getVsrApiLevel(mDevice) <= R_API_LEVEL 1809 && FEATURE_OPENGLES_DEQP_LEVEL.equals(featureName)) { 1810 final int claimedDeqpLevel = DEQP_LEVEL_R_2020; 1811 CLog.d("Device level is %d due to VSR R", claimedDeqpLevel); 1812 final boolean shouldRunCaselist = claimedDeqpLevel >= minimumLevel; 1813 CLog.d("Running caselist? %b", shouldRunCaselist); 1814 return shouldRunCaselist; 1815 } 1816 1817 CLog.d("Running caselist unconditionally"); 1818 1819 return true; 1820 } 1821 1822 /** 1823 * Check if device supports OpenGL ES version. 1824 */ isSupportedGles(ITestDevice device, int requiredMajorVersion, int requiredMinorVersion)1825 private static boolean isSupportedGles(ITestDevice device, int requiredMajorVersion, 1826 int requiredMinorVersion) throws DeviceNotAvailableException { 1827 String roOpenglesVersion = device.getProperty("ro.opengles.version"); 1828 1829 if (roOpenglesVersion == null) 1830 return false; 1831 1832 int intValue = Integer.parseInt(roOpenglesVersion); 1833 1834 int majorVersion = ((intValue & 0xffff0000) >> 16); 1835 int minorVersion = (intValue & 0xffff); 1836 1837 return (majorVersion > requiredMajorVersion) 1838 || (majorVersion == requiredMajorVersion && minorVersion >= requiredMinorVersion); 1839 } 1840 1841 /** 1842 * Query if rendertarget is supported 1843 */ isSupportedGlesRenderConfig(BatchRunConfiguration runConfig)1844 private boolean isSupportedGlesRenderConfig(BatchRunConfiguration runConfig) 1845 throws DeviceNotAvailableException, CapabilityQueryFailureException { 1846 // query if configuration is supported 1847 final StringBuilder configCommandLine = 1848 new StringBuilder(getRunConfigDisplayCmdLine(runConfig)); 1849 if (configCommandLine.length() != 0) { 1850 configCommandLine.append(" "); 1851 } 1852 configCommandLine.append("--deqp-gl-major-version="); 1853 configCommandLine.append(getGlesMajorVersion()); 1854 configCommandLine.append(" --deqp-gl-minor-version="); 1855 configCommandLine.append(getGlesMinorVersion()); 1856 1857 final String commandLine = configCommandLine.toString(); 1858 1859 // check for cached result first 1860 if (mConfigQuerySupportCache.containsKey(commandLine)) { 1861 return mConfigQuerySupportCache.get(commandLine); 1862 } 1863 1864 final boolean supported = queryIsSupportedConfigCommandLine(commandLine); 1865 mConfigQuerySupportCache.put(commandLine, supported); 1866 return supported; 1867 } 1868 queryIsSupportedConfigCommandLine(String deqpCommandLine)1869 private boolean queryIsSupportedConfigCommandLine(String deqpCommandLine) 1870 throws DeviceNotAvailableException, CapabilityQueryFailureException { 1871 final String instrumentationName = 1872 "com.drawelements.deqp/com.drawelements.deqp.platformutil.DeqpPlatformCapabilityQueryInstrumentation"; 1873 final String command = String.format( 1874 "am instrument %s -w -e deqpQueryType renderConfigSupported -e deqpCmdLine \"%s\"" 1875 + " %s", 1876 AbiUtils.createAbiFlag(mAbi.getName()), deqpCommandLine, instrumentationName); 1877 1878 final PlatformQueryInstrumentationParser parser = new PlatformQueryInstrumentationParser(); 1879 mDevice.executeShellCommand(command, parser); 1880 parser.flush(); 1881 1882 if (parser.wasSuccessful() && parser.getResultCode() == 0 && 1883 parser.getResultMap().containsKey("Supported")) { 1884 if ("Yes".equals(parser.getResultMap().get("Supported"))) { 1885 return true; 1886 } else if ("No".equals(parser.getResultMap().get("Supported"))) { 1887 return false; 1888 } else { 1889 CLog.e("Capability query did not return a result"); 1890 throw new CapabilityQueryFailureException(); 1891 } 1892 } else if (parser.wasSuccessful()) { 1893 CLog.e("Failed to run capability query. Code: %d, Result: %s", 1894 parser.getResultCode(), parser.getResultMap().toString()); 1895 throw new CapabilityQueryFailureException(); 1896 } else { 1897 CLog.e("Failed to run capability query"); 1898 throw new CapabilityQueryFailureException(); 1899 } 1900 } 1901 1902 /** 1903 * Return feature set supported by the device, mapping integer-valued features to their values 1904 */ getDeviceFeatures(ITestDevice device)1905 private Map<String, Optional<Integer>> getDeviceFeatures(ITestDevice device) 1906 throws DeviceNotAvailableException, CapabilityQueryFailureException { 1907 if (mDeviceFeatures == null) { 1908 mDeviceFeatures = queryDeviceFeatures(device); 1909 } 1910 return mDeviceFeatures; 1911 } 1912 1913 /** 1914 * Query feature set supported by the device 1915 */ queryDeviceFeatures(ITestDevice device)1916 private static Map<String, Optional<Integer>> queryDeviceFeatures(ITestDevice device) 1917 throws DeviceNotAvailableException, CapabilityQueryFailureException { 1918 // NOTE: Almost identical code in BaseDevicePolicyTest#hasDeviceFeatures 1919 // TODO: Move this logic to ITestDevice. 1920 String command = "pm list features"; 1921 String commandOutput = device.executeShellCommand(command); 1922 1923 // Extract the id of the new user. 1924 Map<String, Optional<Integer>> availableFeatures = new HashMap<>(); 1925 for (String feature: commandOutput.split("\\s+")) { 1926 // Each line in the output of the command has the format "feature:{FEATURE_NAME}", 1927 // optionally followed by "={FEATURE_VERSION}". 1928 String[] tokens = feature.split(":|="); 1929 if (tokens.length < 2 || !"feature".equals(tokens[0])) { 1930 CLog.e("Failed parse features. Unexpect format on line \"%s\"", feature); 1931 throw new CapabilityQueryFailureException(); 1932 } 1933 final String featureName = tokens[1]; 1934 Optional<Integer> featureValue = Optional.empty(); 1935 if (tokens.length > 2) { 1936 try { 1937 // Integer.decode, rather than Integer.parseInt, is used here since some 1938 // feature versions may be presented in decimal and others in hexadecimal. 1939 featureValue = Optional.of(Integer.decode(tokens[2])); 1940 } catch (NumberFormatException numberFormatException) { 1941 CLog.e("Failed parse features. Feature value \"%s\" was not an integer on " 1942 + "line \"%s\"", tokens[2], feature); 1943 throw new CapabilityQueryFailureException(); 1944 } 1945 1946 } 1947 availableFeatures.put(featureName, featureValue); 1948 } 1949 return availableFeatures; 1950 } 1951 isPortraitClassRotation(String rotation)1952 private boolean isPortraitClassRotation(String rotation) { 1953 return BatchRunConfiguration.ROTATION_PORTRAIT.equals(rotation) || 1954 BatchRunConfiguration.ROTATION_REVERSE_PORTRAIT.equals(rotation); 1955 } 1956 isLandscapeClassRotation(String rotation)1957 private boolean isLandscapeClassRotation(String rotation) { 1958 return BatchRunConfiguration.ROTATION_LANDSCAPE.equals(rotation) || 1959 BatchRunConfiguration.ROTATION_REVERSE_LANDSCAPE.equals(rotation); 1960 } 1961 checkRecognizedPackage()1962 private void checkRecognizedPackage() { 1963 if (!isRecognizedPackage()) { 1964 throw new IllegalStateException("dEQP runner was created with illegal package name"); 1965 } 1966 } 1967 isRecognizedPackage()1968 private boolean isRecognizedPackage() { 1969 return "dEQP-EGL".equals(mDeqpPackage) || "dEQP-GLES2".equals(mDeqpPackage) 1970 || "dEQP-GLES3".equals(mDeqpPackage) || "dEQP-GLES31".equals(mDeqpPackage) 1971 || "dEQP-VK".equals(mDeqpPackage); 1972 } 1973 1974 /** 1975 * Parse EGL nature from package name 1976 */ isEglPackage()1977 private boolean isEglPackage() { 1978 checkRecognizedPackage(); 1979 return "dEQP-EGL".equals(mDeqpPackage); 1980 } 1981 1982 /** 1983 * Parse gl nature from package name 1984 */ isOpenGlEsPackage()1985 private boolean isOpenGlEsPackage() { 1986 checkRecognizedPackage(); 1987 return "dEQP-GLES2".equals(mDeqpPackage) || "dEQP-GLES3".equals(mDeqpPackage) 1988 || "dEQP-GLES31".equals(mDeqpPackage); 1989 } 1990 1991 /** 1992 * Parse vulkan nature from package name 1993 */ isVulkanPackage()1994 private boolean isVulkanPackage() { 1995 checkRecognizedPackage(); 1996 return "dEQP-VK".equals(mDeqpPackage); 1997 } 1998 1999 /** 2000 * Check GL support (based on package name) 2001 */ isSupportedGles()2002 private boolean isSupportedGles() throws DeviceNotAvailableException { 2003 return isSupportedGles(mDevice, getGlesMajorVersion(), getGlesMinorVersion()); 2004 } 2005 2006 /** 2007 * Get GL major version (based on package name) 2008 */ getGlesMajorVersion()2009 private int getGlesMajorVersion() { 2010 if ("dEQP-GLES2".equals(mDeqpPackage)) { 2011 return 2; 2012 } else if ("dEQP-GLES3".equals(mDeqpPackage)) { 2013 return 3; 2014 } else if ("dEQP-GLES31".equals(mDeqpPackage)) { 2015 return 3; 2016 } else { 2017 throw new IllegalStateException("getGlesMajorVersion called for non gles pkg"); 2018 } 2019 } 2020 2021 /** 2022 * Get GL minor version (based on package name) 2023 */ getGlesMinorVersion()2024 private int getGlesMinorVersion() { 2025 if ("dEQP-GLES2".equals(mDeqpPackage)) { 2026 return 0; 2027 } else if ("dEQP-GLES3".equals(mDeqpPackage)) { 2028 return 0; 2029 } else if ("dEQP-GLES31".equals(mDeqpPackage)) { 2030 return 1; 2031 } else { 2032 throw new IllegalStateException("getGlesMinorVersion called for non gles pkg"); 2033 } 2034 } 2035 getPatternFilters(List<String> filters)2036 private static List<Pattern> getPatternFilters(List<String> filters) { 2037 List<Pattern> patterns = new ArrayList<Pattern>(); 2038 for (String filter : filters) { 2039 if (filter.contains("*")) { 2040 patterns.add(Pattern.compile(filter.replace(".","\\.").replace("*",".*"))); 2041 } 2042 } 2043 return patterns; 2044 } 2045 getNonPatternFilters(List<String> filters)2046 private static Set<String> getNonPatternFilters(List<String> filters) { 2047 Set<String> nonPatternFilters = new HashSet<String>(); 2048 for (String filter : filters) { 2049 if (filter.startsWith("#") || filter.isEmpty()) { 2050 // Skip comments and empty lines 2051 continue; 2052 } 2053 if (!filter.contains("*")) { 2054 // Deqp usesly only dots for separating between parts of the names 2055 // Convert last dot to hash if needed. 2056 if (!filter.contains("#")) { 2057 int lastSeparator = filter.lastIndexOf('.'); 2058 String filterWithHash = filter.substring(0, lastSeparator) + "#" + 2059 filter.substring(lastSeparator + 1, filter.length()); 2060 nonPatternFilters.add(filterWithHash); 2061 } 2062 else { 2063 nonPatternFilters.add(filter); 2064 } 2065 } 2066 } 2067 return nonPatternFilters; 2068 } 2069 matchesAny(TestDescription test, List<Pattern> patterns)2070 private static boolean matchesAny(TestDescription test, List<Pattern> patterns) { 2071 for (Pattern pattern : patterns) { 2072 if (pattern.matcher(test.toString()).matches()) { 2073 return true; 2074 } 2075 } 2076 return false; 2077 } 2078 2079 /** 2080 * Filter tests with the option of filtering by pattern. 2081 * 2082 * '*' is 0 or more characters. 2083 * '.' is interpreted verbatim. 2084 */ filterTests(Map<TestDescription, Set<BatchRunConfiguration>> tests, List<String> includeFilters, List<String> excludeFilters)2085 private static void filterTests(Map<TestDescription, Set<BatchRunConfiguration>> tests, 2086 List<String> includeFilters, 2087 List<String> excludeFilters) { 2088 // We could filter faster by building the test case tree. 2089 // Let's see if this is fast enough. 2090 Set<String> includeStrings = getNonPatternFilters(includeFilters); 2091 Set<String> excludeStrings = getNonPatternFilters(excludeFilters); 2092 List<Pattern> includePatterns = getPatternFilters(includeFilters); 2093 List<Pattern> excludePatterns = getPatternFilters(excludeFilters); 2094 2095 List<TestDescription> testList = new ArrayList<>(tests.keySet()); 2096 for (TestDescription test : testList) { 2097 if (excludeStrings.contains(test.toString())) { 2098 tests.remove(test); // remove test if explicitly excluded 2099 continue; 2100 } 2101 boolean includesExist = !includeStrings.isEmpty() || !includePatterns.isEmpty(); 2102 boolean testIsIncluded = includeStrings.contains(test.toString()) 2103 || matchesAny(test, includePatterns); 2104 if ((includesExist && !testIsIncluded) || matchesAny(test, excludePatterns)) { 2105 // if this test isn't included and other tests are, 2106 // or if test matches exclude pattern, exclude test 2107 tests.remove(test); 2108 } 2109 } 2110 } 2111 2112 /** 2113 * Read each line from a file. 2114 */ readFile(Collection<String> lines, File file)2115 static private void readFile(Collection<String> lines, File file) throws FileNotFoundException { 2116 if (!file.canRead()) { 2117 CLog.e("Failed to read file '%s'", file.getPath()); 2118 throw new FileNotFoundException(); 2119 } 2120 try (Reader plainReader = new FileReader(file); 2121 BufferedReader reader = new BufferedReader(plainReader)) { 2122 String line = ""; 2123 while ((line = reader.readLine()) != null) { 2124 // TOOD: Sanity check filter 2125 lines.add(line); 2126 } 2127 // Rely on try block to autoclose 2128 } 2129 catch (IOException e) 2130 { 2131 throw new RuntimeException("Failed to read file '" + file.getPath() + "': " + 2132 e.getMessage()); 2133 } 2134 } 2135 2136 /** 2137 * Prints filters into debug log stream, limiting to 20 entries. 2138 */ printFilters(List<String> filters)2139 static private void printFilters(List<String> filters) { 2140 int numPrinted = 0; 2141 for (String filter : filters) { 2142 CLog.d(" %s", filter); 2143 if (++numPrinted == 20) { 2144 CLog.d(" ... AND %d others", filters.size() - numPrinted); 2145 break; 2146 } 2147 } 2148 } 2149 2150 /** 2151 * Loads tests into mTestInstances based on the options. Assumes 2152 * that no tests have been loaded for this instance before. 2153 */ loadTests()2154 private void loadTests() { 2155 if (mTestInstances != null) throw new AssertionError("Re-load of tests not supported"); 2156 2157 // Note: This is specifically a LinkedHashMap to guarantee that tests are iterated 2158 // in the insertion order. 2159 mTestInstances = new LinkedHashMap<>(); 2160 2161 try { 2162 File testlist = new File(mBuildHelper.getTestsDir(), mCaselistFile); 2163 if (!testlist.isFile()) { 2164 // Finding file in sub directory if no matching file in the first layer of 2165 // testdir. 2166 testlist = FileUtil.findFile(mBuildHelper.getTestsDir(), mCaselistFile); 2167 if (testlist == null || !testlist.isFile()) { 2168 throw new FileNotFoundException("Cannot find deqp test list file: " 2169 + mCaselistFile); 2170 } 2171 } 2172 addTestsToInstancesMap( 2173 testlist, 2174 mConfigName, 2175 mScreenRotation, 2176 mSurfaceType, 2177 mConfigRequired, 2178 mTestInstances); 2179 } 2180 catch (FileNotFoundException e) { 2181 throw new RuntimeException("Cannot read deqp test list file: " + mCaselistFile); 2182 } 2183 2184 try 2185 { 2186 if (isIncrementalDeqpRun()) { 2187 for (String testFile : mIncrementalDeqpIncludeFiles) { 2188 CLog.d("Read incremental dEQP include file '%s'", testFile); 2189 File file = new File(mBuildHelper.getTestsDir(), testFile); 2190 if (!file.isFile()) { 2191 // Find file in sub directory if no matching file in the first layer of 2192 // testdir. 2193 file = FileUtil.findFile(mBuildHelper.getTestsDir(), testFile); 2194 if (file == null || !file.isFile()) { 2195 throw new FileNotFoundException( 2196 "Cannot find incremental dEQP include file: " + testFile); 2197 } 2198 } 2199 readFile(mIncrementalDeqpIncludeTests, file); 2200 } 2201 } 2202 for (String filterFile : mIncludeFilterFiles) { 2203 CLog.d("Read include filter file '%s'", filterFile); 2204 File file = new File(mBuildHelper.getTestsDir(), filterFile); 2205 readFile(mIncludeFilters, file); 2206 } 2207 for (String filterFile : mExcludeFilterFiles) { 2208 CLog.d("Read exclude filter file '%s'", filterFile); 2209 File file = new File(mBuildHelper.getTestsDir(), filterFile); 2210 readFile(mExcludeFilters, file); 2211 } 2212 } 2213 catch (FileNotFoundException e) { 2214 throw new HarnessRuntimeException("Cannot read deqp filter list file." + e, 2215 TestErrorIdentifier.TEST_ABORTED); 2216 } 2217 2218 CLog.d("Include filters:"); 2219 printFilters(mIncludeFilters); 2220 CLog.d("Exclude filters:"); 2221 printFilters(mExcludeFilters); 2222 2223 long originalTestCount = mTestInstances.size(); 2224 CLog.i("Num tests before filtering: %d", originalTestCount); 2225 if ((!mIncludeFilters.isEmpty() || !mExcludeFilters.isEmpty()) && originalTestCount > 0) { 2226 filterTests(mTestInstances, mIncludeFilters, mExcludeFilters); 2227 2228 // Update runtime estimation hint. 2229 if (mRuntimeHint != -1) { 2230 mRuntimeHint = (mRuntimeHint * mTestInstances.size()) / originalTestCount; 2231 } 2232 } 2233 CLog.i("Num tests after filtering: %d", mTestInstances.size()); 2234 } 2235 2236 /** 2237 * Set up the test environment. 2238 */ setupTestEnvironment()2239 private void setupTestEnvironment() throws DeviceNotAvailableException { 2240 try { 2241 // Get the system into a known state. 2242 // Clear ANGLE Global.Settings values 2243 mDevice.executeShellCommand("settings delete global angle_gl_driver_selection_pkgs"); 2244 mDevice.executeShellCommand("settings delete global angle_gl_driver_selection_values"); 2245 2246 // ANGLE 2247 if (mAngle.equals(ANGLE_VULKAN)) { 2248 CLog.i("Configuring ANGLE to use: " + mAngle); 2249 // Force dEQP to use ANGLE 2250 mDevice.executeShellCommand( 2251 "settings put global angle_gl_driver_selection_pkgs " + DEQP_ONDEVICE_PKG); 2252 mDevice.executeShellCommand( 2253 "settings put global angle_gl_driver_selection_values angle"); 2254 // Configure ANGLE to use Vulkan 2255 mDevice.executeShellCommand("setprop debug.angle.backend 2"); 2256 } else if (mAngle.equals(ANGLE_OPENGLES)) { 2257 CLog.i("Configuring ANGLE to use: " + mAngle); 2258 // Force dEQP to use ANGLE 2259 mDevice.executeShellCommand( 2260 "settings put global angle_gl_driver_selection_pkgs " + DEQP_ONDEVICE_PKG); 2261 mDevice.executeShellCommand( 2262 "settings put global angle_gl_driver_selection_values angle"); 2263 // Configure ANGLE to use Vulkan 2264 mDevice.executeShellCommand("setprop debug.angle.backend 0"); 2265 } 2266 } catch (DeviceNotAvailableException ex) { 2267 // chain forward 2268 CLog.e("Failed to set up ANGLE correctly."); 2269 throw new DeviceNotAvailableException("Device not available", ex, 2270 mDevice.getSerialNumber()); 2271 } 2272 } 2273 2274 /** 2275 * Clean up the test environment. 2276 */ teardownTestEnvironment()2277 private void teardownTestEnvironment() throws DeviceNotAvailableException { 2278 // ANGLE 2279 try { 2280 CLog.i("Cleaning up ANGLE"); 2281 // Stop forcing dEQP to use ANGLE 2282 mDevice.executeShellCommand("settings delete global angle_gl_driver_selection_pkgs"); 2283 mDevice.executeShellCommand("settings delete global angle_gl_driver_selection_values"); 2284 } catch (DeviceNotAvailableException ex) { 2285 // chain forward 2286 CLog.e("Failed to clean up ANGLE correctly."); 2287 throw new DeviceNotAvailableException("Device not available", ex, 2288 mDevice.getSerialNumber()); 2289 } 2290 } 2291 2292 /** 2293 * {@inheritDoc} 2294 */ 2295 @Override run(ITestInvocationListener listener)2296 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 2297 final HashMap<String, Metric> emptyMap = new HashMap<>(); 2298 // If sharded, split() will load the tests. 2299 if (mTestInstances == null) { 2300 loadTests(); 2301 } 2302 2303 mRemainingTests = new HashSet<>(mTestInstances.keySet()); 2304 long startTime = System.currentTimeMillis(); 2305 listener.testRunStarted(getId(), mRemainingTests.size()); 2306 2307 try { 2308 if (mRemainingTests.isEmpty()) { 2309 CLog.d("No tests to run."); 2310 return; 2311 } 2312 final boolean isSupportedApi = (isOpenGlEsPackage() && isSupportedGles()) 2313 || (isVulkanPackage() && isSupportedVulkan()) 2314 || (!isOpenGlEsPackage() && !isVulkanPackage()); 2315 if (mCollectTestsOnly 2316 || !isSupportedApi 2317 || !claimedDeqpLevelIsRecentEnough()) { 2318 // Pass all tests trivially if: 2319 // - we are collecting the names of the tests only, or 2320 // - the relevant API is not supported, or 2321 // - the device's feature flags do not claim to pass the tests 2322 fakePassTests(listener); 2323 } else if (!mRemainingTests.isEmpty()) { 2324 mInstanceListerner.setSink(listener); 2325 mDeviceRecovery.setDevice(mDevice); 2326 setupTestEnvironment(); 2327 runTests(); 2328 teardownTestEnvironment(); 2329 } 2330 } catch (CapabilityQueryFailureException ex) { 2331 // Platform is not behaving correctly, for example crashing when trying to create 2332 // a window. Instead of silently failing, signal failure by leaving the rest of the 2333 // test cases in "NotExecuted" state 2334 CLog.e("Capability query failed - leaving tests unexecuted."); 2335 } finally { 2336 listener.testRunEnded(System.currentTimeMillis() - startTime, emptyMap); 2337 } 2338 } 2339 2340 /** 2341 * {@inheritDoc} 2342 */ 2343 @Override addIncludeFilter(String filter)2344 public void addIncludeFilter(String filter) { 2345 mIncludeFilters.add(filter); 2346 } 2347 2348 /** 2349 * {@inheritDoc} 2350 */ 2351 @Override addAllIncludeFilters(Set<String> filters)2352 public void addAllIncludeFilters(Set<String> filters) { 2353 mIncludeFilters.addAll(filters); 2354 } 2355 2356 /** 2357 * {@inheritDoc} 2358 */ 2359 @Override getIncludeFilters()2360 public Set<String> getIncludeFilters() { 2361 return new HashSet<>(mIncludeFilters); 2362 } 2363 2364 /** 2365 * {@inheritDoc} 2366 */ 2367 @Override clearIncludeFilters()2368 public void clearIncludeFilters() { 2369 mIncludeFilters.clear(); 2370 } 2371 2372 /** 2373 * {@inheritDoc} 2374 */ 2375 @Override addExcludeFilter(String filter)2376 public void addExcludeFilter(String filter) { 2377 mExcludeFilters.add(filter); 2378 } 2379 2380 /** 2381 * {@inheritDoc} 2382 */ 2383 @Override addAllExcludeFilters(Set<String> filters)2384 public void addAllExcludeFilters(Set<String> filters) { 2385 mExcludeFilters.addAll(filters); 2386 } 2387 2388 /** 2389 * {@inheritDoc} 2390 */ 2391 @Override getExcludeFilters()2392 public Set<String> getExcludeFilters() { 2393 return new HashSet<>(mExcludeFilters); 2394 } 2395 2396 /** 2397 * {@inheritDoc} 2398 */ 2399 @Override clearExcludeFilters()2400 public void clearExcludeFilters() { 2401 mExcludeFilters.clear(); 2402 } 2403 2404 /** 2405 * {@inheritDoc} 2406 */ 2407 @Override setCollectTestsOnly(boolean collectTests)2408 public void setCollectTestsOnly(boolean collectTests) { 2409 mCollectTestsOnly = collectTests; 2410 } 2411 2412 /** 2413 * These methods are for testing. 2414 */ addIncrementalDeqpIncludeTest(String test)2415 public void addIncrementalDeqpIncludeTest(String test) { 2416 mIncrementalDeqpIncludeTests.add(test); 2417 } 2418 2419 /** 2420 * These methods are for testing. 2421 */ addIncrementalDeqpIncludeTests(Collection<String> tests)2422 public void addIncrementalDeqpIncludeTests(Collection<String> tests) { 2423 mIncrementalDeqpIncludeTests.addAll(tests); 2424 } 2425 copyOptions(DeqpTestRunner destination, DeqpTestRunner source)2426 private static void copyOptions(DeqpTestRunner destination, DeqpTestRunner source) { 2427 destination.mUnresponsiveCmdTimeoutMs = source.mUnresponsiveCmdTimeoutMs; 2428 destination.mDeqpPackage = source.mDeqpPackage; 2429 destination.mConfigName = source.mConfigName; 2430 destination.mCaselistFile = source.mCaselistFile; 2431 destination.mScreenRotation = source.mScreenRotation; 2432 destination.mSurfaceType = source.mSurfaceType; 2433 destination.mConfigRequired = source.mConfigRequired; 2434 destination.mIncludeFilters = new ArrayList<>(source.mIncludeFilters); 2435 destination.mIncludeFilterFiles = new ArrayList<>(source.mIncludeFilterFiles); 2436 destination.mExcludeFilters = new ArrayList<>(source.mExcludeFilters); 2437 destination.mExcludeFilterFiles = new ArrayList<>(source.mExcludeFilterFiles); 2438 destination.mAbi = source.mAbi; 2439 destination.mLogData = source.mLogData; 2440 destination.mCollectTestsOnly = source.mCollectTestsOnly; 2441 destination.mAngle = source.mAngle; 2442 destination.mDisableWatchdog = source.mDisableWatchdog; 2443 destination.mIncrementalDeqpIncludeFiles = new ArrayList<>(source.mIncrementalDeqpIncludeFiles); 2444 2445 } 2446 2447 /** 2448 * Helper to update the RuntimeHint of the tests after being sharded. 2449 */ updateRuntimeHint(long originalSize, Collection<IRemoteTest> runners)2450 private void updateRuntimeHint(long originalSize, Collection<IRemoteTest> runners) { 2451 if (originalSize > 0) { 2452 long fullRuntimeMs = getRuntimeHint(); 2453 for (IRemoteTest remote: runners) { 2454 DeqpTestRunner runner = (DeqpTestRunner)remote; 2455 long shardRuntime = (fullRuntimeMs * runner.mTestInstances.size()) / originalSize; 2456 runner.mRuntimeHint = shardRuntime; 2457 } 2458 } 2459 } 2460 2461 /** 2462 * {@inheritDoc} 2463 */ 2464 @Override split()2465 public Collection<IRemoteTest> split() { 2466 if (mTestInstances != null) { 2467 throw new AssertionError("Re-splitting or splitting running instance?"); 2468 } 2469 // \todo [2015-11-23 kalle] If we split to batches at shard level, we could 2470 // basically get rid of batching. Except that sharding is optional? 2471 2472 // Assume that tests have not been yet loaded. 2473 loadTests(); 2474 2475 Collection<IRemoteTest> runners = new ArrayList<>(); 2476 // NOTE: Use linked hash map to keep the insertion order in iteration 2477 Map<TestDescription, Set<BatchRunConfiguration>> currentSet = new LinkedHashMap<>(); 2478 Map<TestDescription, Set<BatchRunConfiguration>> iterationSet = this.mTestInstances; 2479 2480 if (iterationSet.keySet().isEmpty()) { 2481 CLog.i("Cannot split deqp tests, no tests to run"); 2482 return null; 2483 } 2484 2485 // Go through tests, split 2486 for (TestDescription test: iterationSet.keySet()) { 2487 currentSet.put(test, iterationSet.get(test)); 2488 if (currentSet.size() >= getBatchSizeLimit()) { 2489 runners.add(new DeqpTestRunner(this, currentSet)); 2490 // NOTE: Use linked hash map to keep the insertion order in iteration 2491 currentSet = new LinkedHashMap<>(); 2492 } 2493 } 2494 runners.add(new DeqpTestRunner(this, currentSet)); 2495 2496 // Compute new runtime hints 2497 updateRuntimeHint(iterationSet.size(), runners); 2498 CLog.i("Split deqp tests into %d shards", runners.size()); 2499 return runners; 2500 } 2501 2502 /** 2503 * {@inheritDoc} 2504 */ 2505 @Override getRuntimeHint()2506 public long getRuntimeHint() { 2507 if (mRuntimeHint != -1) { 2508 return mRuntimeHint; 2509 } 2510 if (mTestInstances == null) { 2511 loadTests(); 2512 } 2513 // Tests normally take something like ~100ms. Some take a 2514 // second. Let's guess 200ms per test. 2515 return 200 * mTestInstances.size(); 2516 } 2517 } 2518