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