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