1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tradefed.testtype; 17 18 import static com.google.common.base.Preconditions.checkState; 19 20 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner; 21 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; 22 import com.android.tradefed.build.IBuildInfo; 23 import com.android.tradefed.config.Option; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.device.ITestDevice; 26 import com.android.tradefed.log.ITestLogger; 27 import com.android.tradefed.log.LogUtil.CLog; 28 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 29 import com.android.tradefed.result.CollectingTestListener; 30 import com.android.tradefed.result.FileInputStreamSource; 31 import com.android.tradefed.result.ITestInvocationListener; 32 import com.android.tradefed.result.InputStreamSource; 33 import com.android.tradefed.result.LogDataType; 34 import com.android.tradefed.result.ResultForwarder; 35 import com.android.tradefed.result.TestDescription; 36 import com.android.tradefed.result.TestRunResult; 37 import com.android.tradefed.util.FileUtil; 38 import com.android.tradefed.util.ICompressionStrategy; 39 import com.android.tradefed.util.ListInstrumentationParser; 40 import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget; 41 42 import com.google.common.annotations.VisibleForTesting; 43 import com.google.common.base.Strings; 44 import com.google.common.collect.ImmutableMap; 45 46 import java.io.File; 47 import java.io.IOException; 48 import java.util.ArrayList; 49 import java.util.Collection; 50 import java.util.HashMap; 51 import java.util.HashSet; 52 import java.util.List; 53 import java.util.Map; 54 import java.util.Set; 55 56 /** 57 * An abstract base class which runs installed instrumentation test(s) and collects execution data 58 * from each test that was run. Subclasses should implement the {@link #getReportFormat()} method 59 * to convert the execution data into a human readable report and log it. 60 */ 61 public abstract class CodeCoverageTestBase<T extends CodeCoverageReportFormat> 62 implements IDeviceTest, IRemoteTest, IBuildReceiver { 63 64 private ITestDevice mDevice = null; 65 private IBuildInfo mBuild = null; 66 67 @Option(name = "package", 68 description = "Only run instrumentation targets with the given package name") 69 private List<String> mPackageFilter = new ArrayList<>(); 70 71 @Option(name = "runner", 72 description = "Only run instrumentation targets with the given test runner") 73 private List<String> mRunnerFilter = new ArrayList<>(); 74 75 @Option( 76 name = "instrumentation-arg", 77 description = "Additional instrumentation arguments to provide to the runner" 78 ) 79 private Map<String, String> mInstrumentationArgs = new HashMap<String, String>(); 80 81 @Option(name = "max-tests-per-chunk", 82 description = "Maximum number of tests to execute in a single call to 'am instrument'. " 83 + "Used to limit the number of tests that need to be re-run if one of them crashes.") 84 private int mMaxTestsPerChunk = Integer.MAX_VALUE; 85 86 @Option(name = "compression-strategy", 87 description = "Class name of an ICompressionStrategy that will be used to compress the " 88 + "coverage report into a single archive file.") 89 private String mCompressionStrategy = "com.android.tradefed.util.ZipCompressionStrategy"; 90 91 /** 92 * {@inheritDoc} 93 */ 94 @Override setDevice(ITestDevice device)95 public void setDevice(ITestDevice device) { 96 mDevice = device; 97 } 98 99 /** 100 * {@inheritDoc} 101 */ 102 @Override getDevice()103 public ITestDevice getDevice() { 104 return mDevice; 105 } 106 107 /** 108 * {@inheritDoc} 109 */ 110 @Override setBuild(IBuildInfo buildInfo)111 public void setBuild(IBuildInfo buildInfo) { 112 mBuild = buildInfo; 113 } 114 115 /** Returns the {@link IBuildInfo} for this invocation. */ getBuild()116 IBuildInfo getBuild() { 117 return mBuild; 118 } 119 120 /** Returns the package filter as set by the --package option(s). */ getPackageFilter()121 List<String> getPackageFilter() { 122 return mPackageFilter; 123 } 124 125 /** Sets the package-filter option for testing. */ 126 @VisibleForTesting setPackageFilter(List<String> packageFilter)127 void setPackageFilter(List<String> packageFilter) { 128 mPackageFilter = packageFilter; 129 } 130 131 /** Returns the runner filter as set by the --runner option(s). */ getRunnerFilter()132 List<String> getRunnerFilter() { 133 return mRunnerFilter; 134 } 135 136 /** Sets the runner-filter option for testing. */ 137 @VisibleForTesting setRunnerFilter(List<String> runnerFilter)138 void setRunnerFilter(List<String> runnerFilter) { 139 mRunnerFilter = runnerFilter; 140 } 141 142 /** Returns the instrumentation arguments as set by the --instrumentation-arg option(s). */ getInstrumentationArgs()143 Map<String, String> getInstrumentationArgs() { 144 return mInstrumentationArgs; 145 } 146 147 /** Sets the instrumentation-arg options for testing. */ 148 @VisibleForTesting setInstrumentationArgs(Map<String, String> instrumentationArgs)149 void setInstrumentationArgs(Map<String, String> instrumentationArgs) { 150 mInstrumentationArgs = ImmutableMap.copyOf(instrumentationArgs); 151 } 152 153 /** Returns the maximum number of tests to run at once as set by --max-tests-per-chunk. */ getMaxTestsPerChunk()154 int getMaxTestsPerChunk() { 155 return mMaxTestsPerChunk; 156 } 157 158 /** Sets the max-tests-per-chunk option for testing. */ 159 @VisibleForTesting setMaxTestsPerChunk(int maxTestsPerChunk)160 void setMaxTestsPerChunk(int maxTestsPerChunk) { 161 mMaxTestsPerChunk = maxTestsPerChunk; 162 } 163 164 /** Returns the compression strategy that should be used to archive the coverage report. */ getCompressionStrategy()165 ICompressionStrategy getCompressionStrategy() { 166 try { 167 Class<?> clazz = Class.forName(mCompressionStrategy); 168 return clazz.asSubclass(ICompressionStrategy.class).newInstance(); 169 } catch (ClassNotFoundException e) { 170 throw new RuntimeException("Unknown compression strategy: %s", e); 171 } catch (ClassCastException e) { 172 String msg = String.format("%s does not implement ICompressionStrategy", 173 mCompressionStrategy); 174 throw new RuntimeException(msg, e); 175 } catch (IllegalAccessException | InstantiationException e) { 176 String msg = String.format("Could not instantiate %s. The compression strategy must " 177 + "have a public no-args constructor.", mCompressionStrategy); 178 throw new RuntimeException(msg, e); 179 } 180 } 181 182 /** Returns the list of output formats to use when generating the coverage report. */ getReportFormat()183 protected abstract List<T> getReportFormat(); 184 185 /** 186 * {@inheritDoc} 187 */ 188 @Override run(final ITestInvocationListener listener)189 public void run(final ITestInvocationListener listener) throws DeviceNotAvailableException { 190 191 File reportDir = null; 192 File reportArchive = null; 193 // Initialize a listener to collect logged coverage files 194 try (CoverageCollectingListener coverageListener = 195 new CoverageCollectingListener(getDevice(), listener)) { 196 197 // Make sure there are some installed instrumentation targets 198 Collection<InstrumentationTarget> instrumentationTargets = getInstrumentationTargets(); 199 if (instrumentationTargets.isEmpty()) { 200 throw new RuntimeException("No instrumentation targets found"); 201 } 202 203 // Run each of the installed instrumentation targets 204 for (InstrumentationTarget target : instrumentationTargets) { 205 // Compute the number of shards to use 206 int numShards = doesRunnerSupportSharding(target) ? getNumberOfShards(target) : 1; 207 208 // Split the test into shards and invoke each chunk separately in order to limit the 209 // number of test methods that need to be re-run if the test crashes. 210 for (int shardIndex = 0; shardIndex < numShards; shardIndex++) { 211 // Run the current shard 212 TestRunResult result = runTest(target, shardIndex, numShards, coverageListener); 213 214 // If the shard ran to completion and the coverage file was generated 215 String coverageFile = result.getRunMetrics().get( 216 CodeCoverageTest.COVERAGE_REMOTE_FILE_LABEL); 217 if (!result.isRunFailure() && getDevice().doesFileExist(coverageFile)) { 218 // Move on to the next shard 219 continue; 220 } 221 222 // Something went wrong with this shard, so re-run the tests individually 223 for (TestDescription identifier : collectTests(target, shardIndex, numShards)) { 224 runTest(target, identifier, coverageListener); 225 } 226 } 227 } 228 229 // Generate the coverage report(s) and log it 230 List<File> measurements = coverageListener.getCoverageFiles(); 231 for (T format : getReportFormat()) { 232 File report = generateCoverageReport(measurements, format); 233 try { 234 doLogReport("coverage", format.getLogDataType(), report, listener); 235 } finally { 236 FileUtil.recursiveDelete(report); 237 } 238 } 239 } catch (IOException e) { 240 // Rethrow 241 throw new RuntimeException(e); 242 } finally { 243 // Cleanup 244 FileUtil.recursiveDelete(reportDir); 245 FileUtil.deleteFile(reportArchive); 246 cleanup(); 247 } 248 } 249 250 /** 251 * Generates a human-readable coverage report from the given execution data. This method is 252 * called after all of the tests have finished running. 253 * 254 * @param executionData The execution data files collected while running the tests. 255 * @param format The output format of the generated coverage report. 256 */ generateCoverageReport(Collection<File> executionData, T format)257 protected abstract File generateCoverageReport(Collection<File> executionData, T format) 258 throws IOException; 259 260 /** 261 * Cleans up any resources allocated during a test run. Called at the end of the 262 * {@link #run(ITestInvocationListener)} after all coverage reports have been logged. This 263 * method is a stub, but can be overridden by subclasses as necessary. 264 */ cleanup()265 protected void cleanup() { } 266 267 /** 268 * Logs the given data with the provided logger. The {@code data} can be a regular file, or a 269 * directory. If the data is a directory, it is compressed into a single archive file before 270 * being logged. 271 * 272 * @param dataName The name to use when logging the data. 273 * @param dataType The {@link LogDataType} of the data. Ignored if {@code data} is a directory. 274 * @param data The data to log. Can be a regular file, or a directory. 275 * @param logger The {@link ITestLogger} with which to log the data. 276 */ doLogReport(String dataName, LogDataType dataType, File data, ITestLogger logger)277 void doLogReport(String dataName, LogDataType dataType, File data, ITestLogger logger) 278 throws IOException { 279 280 // If the data is a directory, compress it first 281 InputStreamSource streamSource; 282 if (data.isDirectory()) { 283 ICompressionStrategy strategy = getCompressionStrategy(); 284 dataType = strategy.getLogDataType(); 285 streamSource = new FileInputStreamSource(strategy.compress(data), true); 286 } else { 287 streamSource = new FileInputStreamSource(data); 288 } 289 290 // Log the data 291 logger.testLog(dataName, dataType, streamSource); 292 streamSource.close(); 293 } 294 295 /** Returns a new {@link ListInstrumentationParser}. Exposed for unit testing. */ internalCreateListInstrumentationParser()296 ListInstrumentationParser internalCreateListInstrumentationParser() { 297 return new ListInstrumentationParser(); 298 } 299 300 /** Returns the list of instrumentation targets to run. */ getInstrumentationTargets()301 Set<InstrumentationTarget> getInstrumentationTargets() 302 throws DeviceNotAvailableException { 303 304 Set<InstrumentationTarget> ret = new HashSet<>(); 305 306 // Run pm list instrumentation to get the available instrumentation targets 307 ListInstrumentationParser parser = internalCreateListInstrumentationParser(); 308 getDevice().executeShellCommand("pm list instrumentation", parser); 309 310 // If the package or runner filters are set, only include targets that match 311 for (InstrumentationTarget target : parser.getInstrumentationTargets()) { 312 List<String> packageFilter = getPackageFilter(); 313 List<String> runnerFilter = getRunnerFilter(); 314 if ((packageFilter.isEmpty() || packageFilter.contains(target.packageName)) && 315 (runnerFilter.isEmpty() || runnerFilter.contains(target.runnerName))) { 316 ret.add(target); 317 } 318 } 319 320 return ret; 321 } 322 323 /** Checks whether the given {@link InstrumentationTarget} supports sharding. */ doesRunnerSupportSharding(InstrumentationTarget target)324 boolean doesRunnerSupportSharding(InstrumentationTarget target) 325 throws DeviceNotAvailableException { 326 // Compare the number of tests for a given shard with the total number of tests 327 return collectTests(target, 0, 2).size() < collectTests(target).size(); 328 } 329 330 /** Returns all of the {@link TestDescription}s for the given target. */ collectTests(InstrumentationTarget target)331 Collection<TestDescription> collectTests(InstrumentationTarget target) 332 throws DeviceNotAvailableException { 333 return collectTests(target, 0, 1); 334 } 335 336 /** Returns all of the {@link TestDescription}s for the given target and shard. */ collectTests( InstrumentationTarget target, int shardIndex, int numShards)337 Collection<TestDescription> collectTests( 338 InstrumentationTarget target, int shardIndex, int numShards) 339 throws DeviceNotAvailableException { 340 341 // Create a runner and enable test collection 342 IRemoteAndroidTestRunner runner = createTestRunner(target, shardIndex, numShards); 343 runner.setTestCollection(true); 344 345 // Run the test and collect the test identifiers 346 CollectingTestListener listener = new CollectingTestListener(); 347 getDevice().runInstrumentationTests(runner, listener); 348 349 return listener.getCurrentRunResults().getCompletedTests(); 350 } 351 352 /** Returns a new {@link IRemoteAndroidTestRunner} instance. Exposed for unit testing. */ internalCreateTestRunner(String packageName, String runnerName)353 IRemoteAndroidTestRunner internalCreateTestRunner(String packageName, String runnerName) { 354 return new RemoteAndroidTestRunner(packageName, runnerName, getDevice().getIDevice()); 355 } 356 357 /** Returns a new {@link IRemoteAndroidTestRunner} instance for the given target and shard. */ createTestRunner(InstrumentationTarget target, int shardIndex, int numShards)358 IRemoteAndroidTestRunner createTestRunner(InstrumentationTarget target, 359 int shardIndex, int numShards) { 360 361 // Get a new IRemoteAndroidTestRunner instance 362 IRemoteAndroidTestRunner ret = internalCreateTestRunner( 363 target.packageName, target.runnerName); 364 365 // Add instrumentation arguments 366 for (Map.Entry<String, String> argEntry : getInstrumentationArgs().entrySet()) { 367 ret.addInstrumentationArg(argEntry.getKey(), argEntry.getValue()); 368 } 369 370 // Add shard options if necessary 371 if (numShards > 1) { 372 ret.addInstrumentationArg("shardIndex", Integer.toString(shardIndex)); 373 ret.addInstrumentationArg("numShards", Integer.toString(numShards)); 374 } 375 376 return ret; 377 } 378 379 /** Computes the number of shards that should be used when invoking the given target. */ getNumberOfShards(InstrumentationTarget target)380 int getNumberOfShards(InstrumentationTarget target) throws DeviceNotAvailableException { 381 double numTests = collectTests(target).size(); 382 return (int)Math.ceil(numTests / getMaxTestsPerChunk()); 383 } 384 385 /** 386 * Runs a single shard from the given {@code target}. 387 * 388 * @param target The instrumentation target to run. 389 * @param shardIndex The index of the shard to run. 390 * @param numShards The total number of shards for this target. 391 * @param listener The {@link ITestInvocationListener} to be notified of tests results. 392 * @return The results for the executed test run. 393 */ runTest(InstrumentationTarget target, int shardIndex, int numShards, ITestInvocationListener listener)394 TestRunResult runTest(InstrumentationTarget target, int shardIndex, int numShards, 395 ITestInvocationListener listener) throws DeviceNotAvailableException { 396 return runTest(createTest(target, shardIndex, numShards), listener); 397 } 398 399 /** 400 * Runs a single test method from the given {@code target}. 401 * 402 * @param target The instrumentation target to run. 403 * @param identifier The individual test method to run. 404 * @param listener The {@link ITestInvocationListener} to be notified of tests results. 405 * @return The results for the executed test run. 406 */ runTest( InstrumentationTarget target, TestDescription identifier, ITestInvocationListener listener)407 TestRunResult runTest( 408 InstrumentationTarget target, 409 TestDescription identifier, 410 ITestInvocationListener listener) 411 throws DeviceNotAvailableException { 412 return runTest(createTest(target, identifier), listener); 413 } 414 415 /** Runs the given {@link InstrumentationTest} and returns the {@link TestRunResult}. */ runTest(InstrumentationTest test, ITestInvocationListener listener)416 TestRunResult runTest(InstrumentationTest test, ITestInvocationListener listener) 417 throws DeviceNotAvailableException { 418 // Run the test, and return the run results 419 CollectingTestListener results = new CollectingTestListener(); 420 test.run(new ResultForwarder(results, listener)); 421 return results.getCurrentRunResults(); 422 } 423 424 /** Returns a new {@link InstrumentationTest}. Exposed for unit testing. */ internalCreateTest()425 InstrumentationTest internalCreateTest() { 426 return new InstrumentationTest(); 427 } 428 429 /** Returns a new {@link InstrumentationTest} for the given target. */ createTest(InstrumentationTarget target)430 InstrumentationTest createTest(InstrumentationTarget target) { 431 // Get a new InstrumentationTest instance 432 InstrumentationTest ret = internalCreateTest(); 433 ret.setDevice(getDevice()); 434 ret.setPackageName(target.packageName); 435 ret.setRunnerName(target.runnerName); 436 437 // Disable rerun mode, we want to stop the tests as soon as we fail. 438 ret.setRerunMode(false); 439 440 // Add instrumentation arguments 441 for (Map.Entry<String, String> argEntry : getInstrumentationArgs().entrySet()) { 442 ret.addInstrumentationArg(argEntry.getKey(), argEntry.getValue()); 443 } 444 ret.addInstrumentationArg("coverage", "true"); 445 446 return ret; 447 } 448 449 /** Returns a new {@link InstrumentationTest} for the identified test on the given target. */ createTest(InstrumentationTarget target, TestDescription identifier)450 InstrumentationTest createTest(InstrumentationTarget target, TestDescription identifier) { 451 // Get a new InstrumentationTest instance 452 InstrumentationTest ret = createTest(target); 453 454 // Set the specific test method to run 455 ret.setClassName(identifier.getClassName()); 456 ret.setMethodName(identifier.getTestName()); 457 458 return ret; 459 } 460 461 /** Returns a new {@link InstrumentationTest} for a particular shard on the given target. */ createTest(InstrumentationTarget target, int shardIndex, int numShards)462 InstrumentationTest createTest(InstrumentationTarget target, int shardIndex, int numShards) { 463 // Get a new InstrumentationTest instance 464 InstrumentationTest ret = createTest(target); 465 466 // Add shard options if necessary 467 if (numShards > 1) { 468 ret.addInstrumentationArg("shardIndex", Integer.toString(shardIndex)); 469 ret.addInstrumentationArg("numShards", Integer.toString(numShards)); 470 } 471 472 return ret; 473 } 474 475 /** A {@link ResultForwarder} which collects coverage files. */ 476 public static class CoverageCollectingListener extends ResultForwarder 477 implements AutoCloseable { 478 479 private ITestDevice mDevice; 480 private List<File> mCoverageFiles = new ArrayList<>(); 481 private File mCoverageDir; 482 private String mCurrentRunName; 483 CoverageCollectingListener(ITestDevice device, ITestInvocationListener... listeners)484 public CoverageCollectingListener(ITestDevice device, ITestInvocationListener... listeners) 485 throws IOException { 486 super(listeners); 487 488 mDevice = device; 489 490 // Initialize a directory to store the coverage files 491 mCoverageDir = FileUtil.createTempDir("execution_data"); 492 } 493 494 /** Returns the list of collected coverage files. */ getCoverageFiles()495 public List<File> getCoverageFiles() { 496 checkState(mCoverageDir != null, "This object is closed"); 497 return mCoverageFiles; 498 } 499 500 /** 501 * {@inheritDoc} 502 */ 503 @Override testLog(String dataName, LogDataType dataType, InputStreamSource dataStream)504 public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) { 505 super.testLog(dataName, dataType, dataStream); 506 checkState(mCoverageDir != null, "This object is closed"); 507 508 // We only care about coverage files 509 if (LogDataType.COVERAGE.equals(dataType)) { 510 // Save coverage data to a temporary location, and don't inform the listeners yet 511 try { 512 File coverageFile = 513 FileUtil.createTempFile(dataName + "_", ".exec", mCoverageDir); 514 FileUtil.writeToFile(dataStream.createInputStream(), coverageFile); 515 mCoverageFiles.add(coverageFile); 516 CLog.d("Got coverage file: %s", coverageFile.getAbsolutePath()); 517 } catch (IOException e) { 518 CLog.e("Failed to save coverage file"); 519 CLog.e(e); 520 } 521 } 522 } 523 524 /** {@inheritDoc} */ 525 @Override testRunStarted(String runName, int testCount)526 public void testRunStarted(String runName, int testCount) { 527 super.testRunStarted(runName, testCount); 528 mCurrentRunName = runName; 529 } 530 531 /** {@inheritDoc} */ 532 @Override testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)533 public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) { 534 // Look for the coverage file path from the run metrics 535 Metric coverageFilePathMetric = 536 runMetrics.get(CodeCoverageTest.COVERAGE_REMOTE_FILE_LABEL); 537 538 if (coverageFilePathMetric != null) { 539 String coverageFilePath = 540 coverageFilePathMetric.getMeasurements().getSingleString(); 541 if (!Strings.isNullOrEmpty(coverageFilePath)) { 542 CLog.d("Coverage file at %s", coverageFilePath); 543 544 // Try to pull the coverage measurements off of the device 545 File coverageFile = null; 546 try { 547 coverageFile = mDevice.pullFile(coverageFilePath); 548 if (coverageFile != null) { 549 try (FileInputStreamSource source = 550 new FileInputStreamSource(coverageFile)) { 551 testLog( 552 mCurrentRunName + "_runtime_coverage", 553 LogDataType.COVERAGE, 554 source); 555 } 556 } else { 557 CLog.w( 558 "Failed to pull coverage file from device: %s", 559 coverageFilePath); 560 } 561 } catch (DeviceNotAvailableException e) { 562 // Nothing we can do, so just log the error. 563 CLog.w(e); 564 } finally { 565 FileUtil.deleteFile(coverageFile); 566 } 567 } 568 } 569 570 super.testRunEnded(elapsedTime, runMetrics); 571 } 572 573 /** {@inheritDoc} */ 574 @Override close()575 public void close() { 576 FileUtil.recursiveDelete(mCoverageDir); 577 mCoverageDir = null; 578 } 579 } 580 } 581