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