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