1 /* 2 * Copyright (C) 2015 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 17 package com.android.tradefed.testtype; 18 19 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner; 20 import com.android.tradefed.config.ConfigurationException; 21 import com.android.tradefed.config.Option; 22 import com.android.tradefed.config.OptionClass; 23 import com.android.tradefed.config.OptionCopier; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.device.ITestDevice; 26 import com.android.tradefed.invoker.TestInformation; 27 import com.android.tradefed.log.LogUtil.CLog; 28 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 29 import com.android.tradefed.result.FailureDescription; 30 import com.android.tradefed.result.ITestInvocationListener; 31 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus; 32 import com.android.tradefed.util.ArrayUtil; 33 import com.android.tradefed.util.ListInstrumentationParser; 34 35 import com.google.common.annotations.VisibleForTesting; 36 37 import org.junit.runner.notification.RunListener; 38 39 import java.io.File; 40 import java.lang.reflect.InvocationTargetException; 41 import java.util.ArrayList; 42 import java.util.Collection; 43 import java.util.Collections; 44 import java.util.HashMap; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Set; 48 import java.util.regex.Pattern; 49 import java.util.regex.PatternSyntaxException; 50 51 /** 52 * A Test that runs an instrumentation test package on given device using the 53 * android.support.test.runner.AndroidJUnitRunner. 54 */ 55 @OptionClass(alias = "android-junit") 56 public class AndroidJUnitTest extends InstrumentationTest 57 implements IRuntimeHintProvider, 58 ITestFileFilterReceiver, 59 ITestFilterReceiver, 60 ITestAnnotationFilterReceiver, 61 IShardableTest { 62 63 /** instrumentation test runner argument key used for including a class/test */ 64 private static final String INCLUDE_CLASS_INST_ARGS_KEY = "class"; 65 /** instrumentation test runner argument key used for excluding a class/test */ 66 private static final String EXCLUDE_CLASS_INST_ARGS_KEY = "notClass"; 67 /** instrumentation test runner argument key used for including a package */ 68 private static final String INCLUDE_PACKAGE_INST_ARGS_KEY = "package"; 69 /** instrumentation test runner argument key used for excluding a package */ 70 private static final String EXCLUDE_PACKAGE_INST_ARGS_KEY = "notPackage"; 71 /** instrumentation test runner argument key used for including a test regex */ 72 private static final String INCLUDE_REGEX_INST_ARGS_KEY = "tests_regex"; 73 /** instrumentation test runner argument key used for adding annotation filter */ 74 private static final String ANNOTATION_INST_ARGS_KEY = "annotation"; 75 /** instrumentation test runner argument key used for adding notAnnotation filter */ 76 private static final String NOT_ANNOTATION_INST_ARGS_KEY = "notAnnotation"; 77 /** instrumentation test runner argument used for adding testFile filter */ 78 private static final String TEST_FILE_INST_ARGS_KEY = "testFile"; 79 /** instrumentation test runner argument used for adding notTestFile filter */ 80 private static final String NOT_TEST_FILE_INST_ARGS_KEY = "notTestFile"; 81 /** instrumentation test runner argument used to specify the shardIndex of the test */ 82 private static final String SHARD_INDEX_INST_ARGS_KEY = "shardIndex"; 83 /** instrumentation test runner argument used to specify the total number of shards */ 84 private static final String NUM_SHARD_INST_ARGS_KEY = "numShards"; 85 /** 86 * instrumentation test runner argument used to enable the new {@link RunListener} order on 87 * device side. 88 */ 89 public static final String NEW_RUN_LISTENER_ORDER_KEY = "newRunListenerMode"; 90 91 /** Options from the collector side helper library. */ 92 public static final String INCLUDE_COLLECTOR_FILTER_KEY = "include-filter-group"; 93 94 public static final String EXCLUDE_COLLECTOR_FILTER_KEY = "exclude-filter-group"; 95 96 private static final String INCLUDE_FILE = "includes.txt"; 97 private static final String EXCLUDE_FILE = "excludes.txt"; 98 99 @Option(name = "runtime-hint", 100 isTimeVal=true, 101 description="The hint about the test's runtime.") 102 private long mRuntimeHint = 60000;// 1 minute 103 104 @Option( 105 name = "include-filter", 106 description = "The include filters of the test name to run.", 107 requiredForRerun = true) 108 private Set<String> mIncludeFilters = new HashSet<>(); 109 110 @Option( 111 name = "exclude-filter", 112 description = "The exclude filters of the test name to run.", 113 requiredForRerun = true) 114 private Set<String> mExcludeFilters = new HashSet<>(); 115 116 @Option( 117 name = "include-annotation", 118 description = "The annotation class name of the test name to run, can be repeated", 119 requiredForRerun = true) 120 private Set<String> mIncludeAnnotation = new HashSet<>(); 121 122 @Option( 123 name = "exclude-annotation", 124 description = "The notAnnotation class name of the test name to run, can be repeated", 125 requiredForRerun = true) 126 private Set<String> mExcludeAnnotation = new HashSet<>(); 127 128 @Option(name = "test-file-include-filter", 129 description="A file containing a list of line separated test classes and optionally" 130 + " methods to include") 131 private File mIncludeTestFile = null; 132 133 @Option(name = "test-file-exclude-filter", 134 description="A file containing a list of line separated test classes and optionally" 135 + " methods to exclude") 136 private File mExcludeTestFile = null; 137 138 @Option(name = "test-filter-dir", 139 description="The device directory path to which the test filtering files are pushed") 140 private String mTestFilterDir = "/data/local/tmp/ajur"; 141 142 @Option( 143 name = "ajur-max-shard", 144 description = "The maximum number of shard we want to allow the AJUR test to shard into" 145 ) 146 private Integer mMaxShard = null; 147 148 @Option( 149 name = "device-listeners", 150 description = 151 "Specify device side instrumentation listeners to be added for the run. " 152 + "Can be repeated. Note that while the ordering here is followed for " 153 + "now, future versions of AndroidJUnitRunner might not preserve the " 154 + "listener ordering." 155 ) 156 private List<String> mExtraDeviceListeners = new ArrayList<>(); 157 158 @Option( 159 name = "use-new-run-listener-order", 160 description = "Enables the new RunListener Order for AJUR." 161 ) 162 // Default to true as it is harmless if not supported. 163 private boolean mNewRunListenerOrderMode = true; 164 165 private String mDeviceIncludeFile = null; 166 private String mDeviceExcludeFile = null; 167 private int mTotalShards = 0; 168 private int mShardIndex = 0; 169 // Flag to avoid re-sharding a test that already was. 170 private boolean mIsSharded = false; 171 AndroidJUnitTest()172 public AndroidJUnitTest() { 173 super(); 174 setEnforceFormat(true); 175 } 176 177 /** 178 * {@inheritDoc} 179 */ 180 @Override getRuntimeHint()181 public long getRuntimeHint() { 182 return mRuntimeHint; 183 } 184 185 /** 186 * {@inheritDoc} 187 */ 188 @Override addIncludeFilter(String filter)189 public void addIncludeFilter(String filter) { 190 mIncludeFilters.add(filter); 191 } 192 193 /** 194 * {@inheritDoc} 195 */ 196 @Override addAllIncludeFilters(Set<String> filters)197 public void addAllIncludeFilters(Set<String> filters) { 198 mIncludeFilters.addAll(filters); 199 } 200 201 /** 202 * {@inheritDoc} 203 */ 204 @Override addExcludeFilter(String filter)205 public void addExcludeFilter(String filter) { 206 mExcludeFilters.add(filter); 207 } 208 209 /** 210 * {@inheritDoc} 211 */ 212 @Override addAllExcludeFilters(Set<String> filters)213 public void addAllExcludeFilters(Set<String> filters) { 214 mExcludeFilters.addAll(filters); 215 } 216 217 /** {@inheritDoc} */ 218 @Override clearIncludeFilters()219 public void clearIncludeFilters() { 220 mIncludeFilters.clear(); 221 } 222 223 /** {@inheritDoc} */ 224 @Override getIncludeFilters()225 public Set<String> getIncludeFilters() { 226 return mIncludeFilters; 227 } 228 229 /** {@inheritDoc} */ 230 @Override getExcludeFilters()231 public Set<String> getExcludeFilters() { 232 return mExcludeFilters; 233 } 234 235 /** {@inheritDoc} */ 236 @Override clearExcludeFilters()237 public void clearExcludeFilters() { 238 mExcludeFilters.clear(); 239 } 240 241 /** {@inheritDoc} */ 242 @Override setIncludeTestFile(File testFile)243 public void setIncludeTestFile(File testFile) { 244 mIncludeTestFile = testFile; 245 } 246 247 /** 248 * {@inheritDoc} 249 */ 250 @Override setExcludeTestFile(File testFile)251 public void setExcludeTestFile(File testFile) { 252 mExcludeTestFile = testFile; 253 } 254 255 /** 256 * {@inheritDoc} 257 */ 258 @Override addIncludeAnnotation(String annotation)259 public void addIncludeAnnotation(String annotation) { 260 mIncludeAnnotation.add(annotation); 261 } 262 263 /** 264 * {@inheritDoc} 265 */ 266 @Override addAllIncludeAnnotation(Set<String> annotations)267 public void addAllIncludeAnnotation(Set<String> annotations) { 268 mIncludeAnnotation.addAll(annotations); 269 } 270 271 /** 272 * {@inheritDoc} 273 */ 274 @Override addExcludeAnnotation(String excludeAnnotation)275 public void addExcludeAnnotation(String excludeAnnotation) { 276 mExcludeAnnotation.add(excludeAnnotation); 277 } 278 279 /** 280 * {@inheritDoc} 281 */ 282 @Override addAllExcludeAnnotation(Set<String> excludeAnnotations)283 public void addAllExcludeAnnotation(Set<String> excludeAnnotations) { 284 mExcludeAnnotation.addAll(excludeAnnotations); 285 } 286 287 /** {@inheritDoc} */ 288 @Override getIncludeAnnotations()289 public Set<String> getIncludeAnnotations() { 290 return mIncludeAnnotation; 291 } 292 293 /** {@inheritDoc} */ 294 @Override getExcludeAnnotations()295 public Set<String> getExcludeAnnotations() { 296 return mExcludeAnnotation; 297 } 298 299 /** {@inheritDoc} */ 300 @Override clearIncludeAnnotations()301 public void clearIncludeAnnotations() { 302 mIncludeAnnotation.clear(); 303 } 304 305 /** {@inheritDoc} */ 306 @Override clearExcludeAnnotations()307 public void clearExcludeAnnotations() { 308 mExcludeAnnotation.clear(); 309 } 310 311 /** {@inheritDoc} */ 312 @Override run(TestInformation testInfo, final ITestInvocationListener listener)313 public void run(TestInformation testInfo, final ITestInvocationListener listener) 314 throws DeviceNotAvailableException { 315 if (getDevice() == null) { 316 throw new IllegalArgumentException("Device has not been set"); 317 } 318 boolean pushedFile = false; 319 // if mIncludeTestFile is set, perform filtering with this file 320 if (mIncludeTestFile != null && mIncludeTestFile.length() > 0) { 321 mDeviceIncludeFile = mTestFilterDir.replaceAll("/$", "") + "/" + INCLUDE_FILE; 322 pushTestFile(mIncludeTestFile, mDeviceIncludeFile, listener); 323 pushedFile = true; 324 // If an explicit include file filter is provided, do not use the package 325 setTestPackageName(null); 326 } 327 328 // if mExcludeTestFile is set, perform filtering with this file 329 if (mExcludeTestFile != null && mExcludeTestFile.length() > 0) { 330 mDeviceExcludeFile = mTestFilterDir.replaceAll("/$", "") + "/" + EXCLUDE_FILE; 331 pushTestFile(mExcludeTestFile, mDeviceExcludeFile, listener); 332 pushedFile = true; 333 } 334 if (mTotalShards > 0 && !isShardable() && mShardIndex != 0) { 335 // If not shardable, only first shard can run. 336 CLog.i("%s is not shardable.", getRunnerName()); 337 return; 338 } 339 super.run(testInfo, listener); 340 if (pushedFile) { 341 // Remove the directory where the files where pushed 342 removeTestFilterDir(); 343 } 344 } 345 346 /** 347 * {@inheritDoc} 348 */ 349 @Override setRunnerArgs(IRemoteAndroidTestRunner runner)350 protected void setRunnerArgs(IRemoteAndroidTestRunner runner) { 351 super.setRunnerArgs(runner); 352 353 // if mIncludeTestFile is set, perform filtering with this file 354 if (mDeviceIncludeFile != null) { 355 runner.addInstrumentationArg(TEST_FILE_INST_ARGS_KEY, mDeviceIncludeFile); 356 } 357 358 // if mExcludeTestFile is set, perform filtering with this file 359 if (mDeviceExcludeFile != null) { 360 runner.addInstrumentationArg(NOT_TEST_FILE_INST_ARGS_KEY, mDeviceExcludeFile); 361 } 362 363 // Split filters into class, notClass, package and notPackage 364 List<String> classArg = new ArrayList<String>(); 365 List<String> notClassArg = new ArrayList<String>(); 366 List<String> packageArg = new ArrayList<String>(); 367 List<String> notPackageArg = new ArrayList<String>(); 368 List<String> regexArg = new ArrayList<String>(); 369 for (String test : mIncludeFilters) { 370 if (isRegex(test)) { 371 regexArg.add(test); 372 } else if (isClassOrMethod(test)) { 373 classArg.add(test); 374 } else { 375 packageArg.add(test); 376 } 377 } 378 for (String test : mExcludeFilters) { 379 // tests_regex doesn't support exclude-filter. Therefore, only check if the filter is 380 // for class/method or package. 381 if (isClassOrMethod(test)) { 382 notClassArg.add(test); 383 } else { 384 notPackageArg.add(test); 385 } 386 } 387 if (!classArg.isEmpty()) { 388 runner.addInstrumentationArg(INCLUDE_CLASS_INST_ARGS_KEY, 389 ArrayUtil.join(",", classArg)); 390 } 391 if (!notClassArg.isEmpty()) { 392 runner.addInstrumentationArg(EXCLUDE_CLASS_INST_ARGS_KEY, 393 ArrayUtil.join(",", notClassArg)); 394 } 395 if (!packageArg.isEmpty()) { 396 runner.addInstrumentationArg(INCLUDE_PACKAGE_INST_ARGS_KEY, 397 ArrayUtil.join(",", packageArg)); 398 } 399 if (!notPackageArg.isEmpty()) { 400 runner.addInstrumentationArg(EXCLUDE_PACKAGE_INST_ARGS_KEY, 401 ArrayUtil.join(",", notPackageArg)); 402 } 403 if (!regexArg.isEmpty()) { 404 String regexFilter; 405 if (regexArg.size() == 1) { 406 regexFilter = regexArg.get(0); 407 } else { 408 Collections.sort(regexArg); 409 regexFilter = "\"(" + ArrayUtil.join("|", regexArg) + ")\""; 410 } 411 runner.addInstrumentationArg(INCLUDE_REGEX_INST_ARGS_KEY, regexFilter); 412 } 413 if (!mIncludeAnnotation.isEmpty()) { 414 runner.addInstrumentationArg(ANNOTATION_INST_ARGS_KEY, 415 ArrayUtil.join(",", mIncludeAnnotation)); 416 } 417 if (!mExcludeAnnotation.isEmpty()) { 418 runner.addInstrumentationArg(NOT_ANNOTATION_INST_ARGS_KEY, 419 ArrayUtil.join(",", mExcludeAnnotation)); 420 } 421 if (mTotalShards > 0 && isShardable()) { 422 runner.addInstrumentationArg(SHARD_INDEX_INST_ARGS_KEY, Integer.toString(mShardIndex)); 423 runner.addInstrumentationArg(NUM_SHARD_INST_ARGS_KEY, Integer.toString(mTotalShards)); 424 } 425 if (mNewRunListenerOrderMode) { 426 runner.addInstrumentationArg( 427 NEW_RUN_LISTENER_ORDER_KEY, Boolean.toString(mNewRunListenerOrderMode)); 428 } 429 // Add the listeners received from Options 430 addDeviceListeners(mExtraDeviceListeners); 431 } 432 433 /** 434 * Push the testFile to the requested destination. This should only be called for a non-null 435 * testFile 436 * 437 * @param testFile file to be pushed from the host to the device. 438 * @param destination the path on the device to which testFile is pushed 439 * @param listener {@link ITestInvocationListener} to report failures. 440 */ pushTestFile(File testFile, String destination, ITestInvocationListener listener)441 private void pushTestFile(File testFile, String destination, ITestInvocationListener listener) 442 throws DeviceNotAvailableException { 443 if (!testFile.canRead() || !testFile.isFile()) { 444 String message = String.format("Cannot read test file %s", testFile.getAbsolutePath()); 445 reportEarlyFailure(listener, message); 446 throw new IllegalArgumentException(message); 447 } 448 ITestDevice device = getDevice(); 449 try { 450 CLog.d("Attempting to push filters to %s", destination); 451 if (!device.pushFile(testFile, destination)) { 452 String message = 453 String.format( 454 "Failed to push file %s to %s for %s in pushTestFile", 455 testFile.getAbsolutePath(), destination, device.getSerialNumber()); 456 reportEarlyFailure(listener, message); 457 throw new RuntimeException(message); 458 } 459 // in case the folder was created as 'root' we make is usable. 460 device.executeShellCommand(String.format("chown -R shell:shell %s", mTestFilterDir)); 461 } catch (DeviceNotAvailableException e) { 462 reportEarlyFailure(listener, e.getMessage()); 463 throw e; 464 } 465 } 466 removeTestFilterDir()467 private void removeTestFilterDir() throws DeviceNotAvailableException { 468 getDevice().deleteFile(mTestFilterDir); 469 } 470 reportEarlyFailure(ITestInvocationListener listener, String errorMessage)471 private void reportEarlyFailure(ITestInvocationListener listener, String errorMessage) { 472 listener.testRunStarted("AndroidJUnitTest_setupError", 0); 473 FailureDescription failure = FailureDescription.create(errorMessage); 474 failure.setFailureStatus(FailureStatus.INFRA_FAILURE); 475 listener.testRunFailed(failure); 476 listener.testRunEnded(0, new HashMap<String, Metric>()); 477 } 478 479 /** 480 * Return if a string is the name of a Class or a Method. 481 */ 482 @VisibleForTesting isClassOrMethod(String filter)483 public boolean isClassOrMethod(String filter) { 484 if (filter.contains("#")) { 485 return true; 486 } 487 String[] parts = filter.split("\\."); 488 if (parts.length > 0) { 489 // FIXME Assume java package names starts with lowercase and class names start with 490 // uppercase. 491 // Return true iff the first character of the last word is uppercase 492 // com.android.foobar.Test 493 return Character.isUpperCase(parts[parts.length - 1].charAt(0)); 494 } 495 return false; 496 } 497 498 /** Return if a string is a regex for filter. */ 499 @VisibleForTesting isRegex(String filter)500 public boolean isRegex(String filter) { 501 // If filter contains any special regex character, return true. 502 // Throw RuntimeException if the regex is invalid. 503 if (Pattern.matches(".*[\\?\\*\\^\\$\\(\\)\\[\\]\\{\\}\\|\\\\].*", filter)) { 504 try { 505 Pattern.compile(filter); 506 } catch (PatternSyntaxException e) { 507 CLog.e("Filter %s is not a valid regular expression string.", filter); 508 throw new RuntimeException(e); 509 } 510 return true; 511 } 512 513 return false; 514 } 515 516 /** 517 * Helper to return if the runner is one that support sharding. 518 */ isShardable()519 private boolean isShardable() { 520 // Edge toward shardable if no explicit runner specified. The runner will be determined 521 // later and if not shardable only the first shard will run. 522 if (getRunnerName() == null) { 523 return true; 524 } 525 return ListInstrumentationParser.SHARDABLE_RUNNERS.contains(getRunnerName()); 526 } 527 528 /** {@inheritDoc} */ 529 @Override split(int shardCount)530 public Collection<IRemoteTest> split(int shardCount) { 531 if (!isShardable()) { 532 return null; 533 } 534 if (mMaxShard != null) { 535 shardCount = Math.min(shardCount, mMaxShard); 536 } 537 if (!mIsSharded && shardCount > 1) { 538 mIsSharded = true; 539 Collection<IRemoteTest> shards = new ArrayList<>(shardCount); 540 for (int index = 0; index < shardCount; index++) { 541 shards.add(getTestShard(shardCount, index)); 542 } 543 return shards; 544 } 545 return null; 546 } 547 getTestShard(int shardCount, int shardIndex)548 private IRemoteTest getTestShard(int shardCount, int shardIndex) { 549 AndroidJUnitTest shard; 550 // ensure we handle runners that extend AndroidJUnitRunner 551 try { 552 shard = this.getClass().getDeclaredConstructor().newInstance(); 553 } catch (InstantiationException 554 | IllegalAccessException 555 | InvocationTargetException 556 | NoSuchMethodException e) { 557 throw new RuntimeException(e); 558 } 559 try { 560 OptionCopier.copyOptions(this, shard); 561 } catch (ConfigurationException e) { 562 CLog.e("Failed to copy instrumentation options: %s", e.getMessage()); 563 } 564 shard.mShardIndex = shardIndex; 565 shard.mTotalShards = shardCount; 566 shard.mIsSharded = true; 567 shard.setAbi(getAbi()); 568 // We approximate the runtime of each shard to be equal since we can't know. 569 shard.mRuntimeHint = mRuntimeHint / shardCount; 570 return shard; 571 } 572 } 573