1 /*
2  * Copyright (C) 2019 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.test.filters;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.Bundle;
22 import android.util.Log;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import org.junit.runner.Description;
27 import org.junit.runner.manipulation.Filter;
28 
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.LinkedHashMap;
33 import java.util.LinkedHashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.StringJoiner;
38 
39 /**
40  * JUnit filter to select tests.
41  *
42  * <p>This filter selects tests specified by package name, class name, and method name. With this
43  * filter, the package and the class options of AndroidJUnitRunner can be superseded. Also the
44  * restriction that prevents using the package and the class options can be mitigated.
45  *
46  * <p><b>Select out tests from Java packages:</b> this option supersedes {@code -e package} option.
47  * <pre>
48  * adb shell am instrument -w \
49  *     -e filter com.android.test.filters.SelectTest \
50  *     -e selectTest package1.,package2. \
51  *     com.tests.pkg/androidx.test.runner.AndroidJUnitRunner
52  * </pre>
53  * Note that the ending {@code .} in package name is mandatory.
54  *
55  * <p><b>Select out test classes:</b> this option supersedes {@code -e class} option.
56  * <pre>
57  * adb shell am instrument -w \
58  *     -e filter com.android.test.filters.SelectTest      \
59  *     -e selectTest package1.ClassA,package2.ClassB \
60  *     com.tests.pkg/androidx.test.runner.AndroidJUnitRunner
61  * </pre>
62  *
63  * <p><b>Select out test methods from Java classes:</b>
64  * <pre>
65  * adb shell am instrument -w \
66  *     -e filter com.android.test.filters.SelectTest                      \
67  *     -e selectTest package1.ClassA#methodX,package2.ClassB#methodY \
68  *     com.tests.pkg/androidx.test.runner.AndroidJUnitRunner
69  * </pre>
70  *
71  * Those options can be used simultaneously. For example
72  * <pre>
73  * adb shell am instrument -w \
74  *     -e filter com.android.test.filters.SelectTest                        \
75  *     -e selectTest package1.,package2.classA,package3.ClassB#methodZ \
76  *     com.tests.pkg/androidx.test.runner.AndroidJUnitRunner
77  * </pre>
78  * will select out all tests in package1, all tests in classA, and ClassB#methodZ test.
79  *
80  * <p>Note that when this option is specified with either {@code -e package} or {@code -e class}
81  * option, filtering behaves as logically conjunction. Other options, such as {@code -e notPackage},
82  * {@code -e notClass}, {@code -e annotation}, and {@code -e notAnnotation}, should work as expected
83  * with this SelectTest option.
84  *
85  * <p>When specified with {@code -e selectTest_verbose true} option, {@link SelectTest} verbosely
86  * logs to logcat while parsing {@code -e selectTest} option.
87  */
88 public class SelectTest extends Filter {
89 
90     private static final String TAG = SelectTest.class.getSimpleName();
91 
92     @VisibleForTesting
93     static final String OPTION_SELECT_TEST = "selectTest";
94     @VisibleForTesting
95     static final String OPTION_SELECT_TEST_VERBOSE = OPTION_SELECT_TEST + "_verbose";
96 
97     private static final String ARGUMENT_ITEM_SEPARATOR = ",";
98     private static final String PACKAGE_NAME_SEPARATOR = ".";
99     private static final String METHOD_SEPARATOR = "#";
100 
101     @Nullable
102     private final PackageSet mPackageSet;
103 
104     /**
105      * Construct {@link SelectTest} filter from instrumentation arguments in {@link Bundle}.
106      *
107      * @param testArgs instrumentation test arguments.
108      */
SelectTest(@onNull Bundle testArgs)109     public SelectTest(@NonNull Bundle testArgs) {
110         mPackageSet = parseSelectTest(testArgs);
111     }
112 
113     @Override
shouldRun(Description description)114     public boolean shouldRun(Description description) {
115         if (mPackageSet == null) {
116             // Accept all tests because this filter is disabled.
117             return true;
118         }
119         String testClassName = description.getClassName();
120         String testMethodName = description.getMethodName();
121         return mPackageSet.accept(testClassName, testMethodName);
122     }
123 
124     @Override
describe()125     public String describe() {
126         return OPTION_SELECT_TEST + "=" + mPackageSet;
127     }
128 
129     /**
130      * Create {@link #OPTION_SELECT_TEST} argument and add it to {@code testArgs}.
131      *
132      * <p>This method is intended to be used at constructor of extended {@link Filter} class.
133      *
134      * @param testArgs instrumentation test arguments.
135      * @param selectTests array of class name to be selected to run.
136      * @return modified instrumentation test arguments. if {@link #OPTION_SELECT_TEST} argument
137      *      already exists in {@code testArgs}, those are prepended before {@code selectTests}.
138      */
139     @NonNull
addSelectTest( @onNull Bundle testArgs, @NonNull String... selectTests)140     protected static Bundle addSelectTest(
141             @NonNull Bundle testArgs, @NonNull String... selectTests) {
142         if (selectTests.length == 0) {
143             return testArgs;
144         }
145         final List<String> selectedTestList = new ArrayList<>();
146         final String selectTestArgs = testArgs.getString(OPTION_SELECT_TEST);
147         if (selectTestArgs != null) {
148             selectedTestList.addAll(Arrays.asList(selectTestArgs.split(ARGUMENT_ITEM_SEPARATOR)));
149         }
150         selectedTestList.addAll(Arrays.asList(selectTests));
151         testArgs.putString(OPTION_SELECT_TEST, join(selectedTestList));
152         return testArgs;
153     }
154 
155     /**
156      * Parse {@code -e selectTest} argument.
157      * @param testArgs instrumentation test arguments.
158      * @return {@link PackageSet} that will filter tests. Returns {@code null} when no
159      *     {@code -e selectTest} option is specified, thus this filter gets disabled.
160      */
161     @Nullable
parseSelectTest(Bundle testArgs)162     private static PackageSet parseSelectTest(Bundle testArgs) {
163         final String selectTestArgs = testArgs.getString(OPTION_SELECT_TEST);
164         if (selectTestArgs == null) {
165             Log.w(TAG, "Disabled because no " + OPTION_SELECT_TEST + " option specified");
166             return null;
167         }
168 
169         final boolean verbose = new Boolean(testArgs.getString(OPTION_SELECT_TEST_VERBOSE));
170         final PackageSet packageSet = new PackageSet(verbose);
171         for (String selectTestArg : selectTestArgs.split(ARGUMENT_ITEM_SEPARATOR)) {
172             packageSet.add(selectTestArg);
173         }
174         return packageSet;
175     }
176 
getPackageName(String selectTestArg)177     private static String getPackageName(String selectTestArg) {
178         int endPackagePos = selectTestArg.lastIndexOf(PACKAGE_NAME_SEPARATOR);
179         return (endPackagePos < 0) ? "" : selectTestArg.substring(0, endPackagePos);
180     }
181 
182     @Nullable
getClassName(String selectTestArg)183     private static String getClassName(String selectTestArg) {
184         if (selectTestArg.endsWith(PACKAGE_NAME_SEPARATOR)) {
185             return null;
186         }
187         int methodSepPos = selectTestArg.indexOf(METHOD_SEPARATOR);
188         return (methodSepPos < 0) ? selectTestArg : selectTestArg.substring(0, methodSepPos);
189     }
190 
191     @Nullable
getMethodName(String selectTestArg)192     private static String getMethodName(String selectTestArg) {
193         int methodSepPos = selectTestArg.indexOf(METHOD_SEPARATOR);
194         return (methodSepPos < 0) ? null : selectTestArg.substring(methodSepPos + 1);
195     }
196 
197     /** Package level filter */
198     private static class PackageSet {
199         private final boolean mVerbose;
200         /**
201          * Java package name to {@link ClassSet} map. To represent package filtering, a map value
202          * can be {@code null}.
203          */
204         private final Map<String, ClassSet> mClassSetMap = new LinkedHashMap<>();
205 
PackageSet(boolean verbose)206         PackageSet(boolean verbose) {
207             mVerbose = verbose;
208         }
209 
add(final String selectTestArg)210         void add(final String selectTestArg) {
211             final String packageName = getPackageName(selectTestArg);
212             final String className = getClassName(selectTestArg);
213 
214             if (className == null) {
215                 ClassSet classSet = mClassSetMap.put(packageName, null); // package filtering.
216                 if (mVerbose) {
217                     logging("Select package " + selectTestArg, classSet != null,
218                             "; supersede " + classSet);
219                 }
220                 return;
221             }
222 
223             ClassSet classSet = mClassSetMap.get(packageName);
224             if (classSet == null) {
225                 if (mClassSetMap.containsKey(packageName)) {
226                     if (mVerbose) {
227                         logging("Select package " + packageName + PACKAGE_NAME_SEPARATOR, true,
228                                 " ignore " + selectTestArg);
229                     }
230                     return;
231                 }
232                 classSet = new ClassSet(mVerbose);
233                 mClassSetMap.put(packageName, classSet);
234             }
235             classSet.add(selectTestArg);
236         }
237 
accept(String className, @Nullable String methodName)238         boolean accept(String className, @Nullable String methodName) {
239             String packageName = getPackageName(className);
240             if (!mClassSetMap.containsKey(packageName)) {
241                 return false;
242             }
243             ClassSet classSet = mClassSetMap.get(packageName);
244             return classSet == null || classSet.accept(className, methodName);
245         }
246 
247         @Override
toString()248         public String toString() {
249             StringJoiner joiner = new StringJoiner(ARGUMENT_ITEM_SEPARATOR);
250             for (String packageName : mClassSetMap.keySet()) {
251                 ClassSet classSet = mClassSetMap.get(packageName);
252                 joiner.add(classSet == null
253                         ? packageName + PACKAGE_NAME_SEPARATOR : classSet.toString());
254             }
255             return joiner.toString();
256         }
257     }
258 
259     /** Class level filter */
260     private static class ClassSet {
261         private final boolean mVerbose;
262         /**
263          * Java class name to set of method names map. To represent class filtering, a map value
264          * can be {@code null}.
265          */
266         private final Map<String, Set<String>> mMethodSetMap = new LinkedHashMap<>();
267 
ClassSet(boolean verbose)268         ClassSet(boolean verbose) {
269             mVerbose = verbose;
270         }
271 
add(String selectTestArg)272         void add(String selectTestArg) {
273             final String className = getClassName(selectTestArg);
274             final String methodName = getMethodName(selectTestArg);
275 
276             if (methodName == null) {
277                 Set<String> methodSet = mMethodSetMap.put(className, null); // class filtering.
278                 if (mVerbose) {
279                     logging("Select class " + selectTestArg, methodSet != null,
280                             "; supersede " + toString(className, methodSet));
281                 }
282                 return;
283             }
284 
285             Set<String> methodSet = mMethodSetMap.get(className);
286             if (methodSet == null) {
287                 if (mMethodSetMap.containsKey(className)) {
288                     if (mVerbose) {
289                         logging("Select class " + className, true, "; ignore " + selectTestArg);
290                     }
291                     return;
292                 }
293                 methodSet = new LinkedHashSet<>();
294                 mMethodSetMap.put(className, methodSet);
295             }
296 
297             methodSet.add(methodName);
298             if (mVerbose) {
299                 logging("Select method " + selectTestArg, false, null);
300             }
301         }
302 
accept(String className, @Nullable String methodName)303         boolean accept(String className, @Nullable String methodName) {
304             if (!mMethodSetMap.containsKey(className)) {
305                 return false;
306             }
307             Set<String> methodSet = mMethodSetMap.get(className);
308             return methodName == null || methodSet == null || methodSet.contains(methodName);
309         }
310 
311         @Override
toString()312         public String toString() {
313             StringJoiner joiner = new StringJoiner(ARGUMENT_ITEM_SEPARATOR);
314             for (String className : mMethodSetMap.keySet()) {
315                 joiner.add(toString(className, mMethodSetMap.get(className)));
316             }
317             return joiner.toString();
318         }
319 
toString(String className, @Nullable Set<String> methodSet)320         private static String toString(String className, @Nullable Set<String> methodSet) {
321             if (methodSet == null) {
322                 return className;
323             }
324             StringJoiner joiner = new StringJoiner(ARGUMENT_ITEM_SEPARATOR);
325             for (String methodName : methodSet) {
326                 joiner.add(className + METHOD_SEPARATOR + methodName);
327             }
328             return joiner.toString();
329         }
330     }
331 
logging(String infoLog, boolean isWarning, String warningLog)332     private static void logging(String infoLog, boolean isWarning, String warningLog) {
333         if (isWarning) {
334             Log.w(TAG, infoLog + warningLog);
335         } else {
336             Log.i(TAG, infoLog);
337         }
338     }
339 
join(Collection<String> list)340     private static String join(Collection<String> list) {
341         StringJoiner joiner = new StringJoiner(ARGUMENT_ITEM_SEPARATOR);
342         for (String text : list) {
343             joiner.add(text);
344         }
345         return joiner.toString();
346     }
347 }
348