1 /* 2 * Copyright (C) 2012 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.test.runner; 17 18 import android.app.Instrumentation; 19 import android.os.Bundle; 20 import android.test.suitebuilder.annotation.LargeTest; 21 import android.test.suitebuilder.annotation.MediumTest; 22 import android.test.suitebuilder.annotation.SmallTest; 23 import android.test.suitebuilder.annotation.Suppress; 24 import android.util.Log; 25 26 import com.android.test.runner.ClassPathScanner.ChainedClassNameFilter; 27 import com.android.test.runner.ClassPathScanner.ExcludePackageNameFilter; 28 import com.android.test.runner.ClassPathScanner.ExternalClassNameFilter; 29 30 import org.junit.runner.Computer; 31 import org.junit.runner.Description; 32 import org.junit.runner.Request; 33 import org.junit.runner.Runner; 34 import org.junit.runner.manipulation.Filter; 35 import org.junit.runners.model.InitializationError; 36 37 import java.io.IOException; 38 import java.io.PrintStream; 39 import java.lang.annotation.Annotation; 40 import java.util.Arrays; 41 import java.util.Collection; 42 import java.util.Collections; 43 import java.util.regex.Pattern; 44 45 /** 46 * Builds a {@link Request} from test classes in given apk paths, filtered on provided set of 47 * restrictions. 48 */ 49 public class TestRequestBuilder { 50 51 private static final String LOG_TAG = "TestRequestBuilder"; 52 53 public static final String LARGE_SIZE = "large"; 54 public static final String MEDIUM_SIZE = "medium"; 55 public static final String SMALL_SIZE = "small"; 56 57 private String[] mApkPaths; 58 private TestLoader mTestLoader; 59 private Filter mFilter = new AnnotationExclusionFilter(Suppress.class); 60 private PrintStream mWriter; 61 private boolean mSkipExecution = false; 62 63 /** 64 * Filter that only runs tests whose method or class has been annotated with given filter. 65 */ 66 private static class AnnotationInclusionFilter extends Filter { 67 68 private final Class<? extends Annotation> mAnnotationClass; 69 70 AnnotationInclusionFilter(Class<? extends Annotation> annotation) { 71 mAnnotationClass = annotation; 72 } 73 74 /** 75 * {@inheritDoc} 76 */ 77 @Override 78 public boolean shouldRun(Description description) { 79 if (description.isTest()) { 80 return description.getAnnotation(mAnnotationClass) != null || 81 description.getTestClass().isAnnotationPresent(mAnnotationClass); 82 } else { 83 // the entire test class/suite should be filtered out if all its methods are 84 // filtered 85 // TODO: This is not efficient since some children may end up being evaluated more 86 // than once. This logic seems to be only necessary for JUnit3 tests. Look into 87 // fixing in upstream 88 for (Description child : description.getChildren()) { 89 if (shouldRun(child)) { 90 return true; 91 } 92 } 93 // no children to run, filter this out 94 return false; 95 } 96 } 97 98 /** 99 * {@inheritDoc} 100 */ 101 @Override 102 public String describe() { 103 return String.format("annotation %s", mAnnotationClass.getName()); 104 } 105 } 106 107 /** 108 * Filter out tests whose method or class has been annotated with given filter. 109 */ 110 private static class AnnotationExclusionFilter extends Filter { 111 112 private final Class<? extends Annotation> mAnnotationClass; 113 114 AnnotationExclusionFilter(Class<? extends Annotation> annotation) { 115 mAnnotationClass = annotation; 116 } 117 118 /** 119 * {@inheritDoc} 120 */ 121 @Override 122 public boolean shouldRun(Description description) { 123 final Class<?> testClass = description.getTestClass(); 124 125 /* Parameterized tests have no test classes. */ 126 if (testClass == null) { 127 return true; 128 } 129 130 if (testClass.isAnnotationPresent(mAnnotationClass) || 131 description.getAnnotation(mAnnotationClass) != null) { 132 return false; 133 } else { 134 return true; 135 } 136 } 137 138 /** 139 * {@inheritDoc} 140 */ 141 @Override 142 public String describe() { 143 return String.format("not annotation %s", mAnnotationClass.getName()); 144 } 145 } 146 147 public TestRequestBuilder(PrintStream writer, String... apkPaths) { 148 mApkPaths = apkPaths; 149 mTestLoader = new TestLoader(writer); 150 } 151 152 /** 153 * Add a test class to be executed. All test methods in this class will be executed. 154 * 155 * @param className 156 */ 157 public void addTestClass(String className) { 158 mTestLoader.loadClass(className); 159 } 160 161 /** 162 * Adds a test method to run. 163 * <p/> 164 * Currently only supports one test method to be run. 165 */ 166 public void addTestMethod(String testClassName, String testMethodName) { 167 Class<?> clazz = mTestLoader.loadClass(testClassName); 168 if (clazz != null) { 169 mFilter = mFilter.intersect(matchParameterizedMethod( 170 Description.createTestDescription(clazz, testMethodName))); 171 } 172 } 173 174 /** 175 * A filter to get around the fact that parameterized tests append "[#]" at 176 * the end of the method names. For instance, "getFoo" would become 177 * "getFoo[0]". 178 */ 179 private static Filter matchParameterizedMethod(final Description target) { 180 return new Filter() { 181 Pattern pat = Pattern.compile(target.getMethodName() + "(\\[[0-9]+\\])?"); 182 183 @Override 184 public boolean shouldRun(Description desc) { 185 if (desc.isTest()) { 186 return target.getClassName().equals(desc.getClassName()) 187 && isMatch(desc.getMethodName()); 188 } 189 190 for (Description child : desc.getChildren()) { 191 if (shouldRun(child)) { 192 return true; 193 } 194 } 195 return false; 196 } 197 198 private boolean isMatch(String first) { 199 return pat.matcher(first).matches(); 200 } 201 202 @Override 203 public String describe() { 204 return String.format("Method %s", target.getDisplayName()); 205 } 206 }; 207 } 208 209 /** 210 * Run only tests with given size 211 * @param testSize 212 */ 213 public void addTestSizeFilter(String testSize) { 214 if (SMALL_SIZE.equals(testSize)) { 215 mFilter = mFilter.intersect(new AnnotationInclusionFilter(SmallTest.class)); 216 } else if (MEDIUM_SIZE.equals(testSize)) { 217 mFilter = mFilter.intersect(new AnnotationInclusionFilter(MediumTest.class)); 218 } else if (LARGE_SIZE.equals(testSize)) { 219 mFilter = mFilter.intersect(new AnnotationInclusionFilter(LargeTest.class)); 220 } else { 221 Log.e(LOG_TAG, String.format("Unrecognized test size '%s'", testSize)); 222 } 223 } 224 225 /** 226 * Only run tests annotated with given annotation class. 227 * 228 * @param annotation the full class name of annotation 229 */ 230 public void addAnnotationInclusionFilter(String annotation) { 231 Class<? extends Annotation> annotationClass = loadAnnotationClass(annotation); 232 if (annotationClass != null) { 233 mFilter = mFilter.intersect(new AnnotationInclusionFilter(annotationClass)); 234 } 235 } 236 237 /** 238 * Skip tests annotated with given annotation class. 239 * 240 * @param notAnnotation the full class name of annotation 241 */ 242 public void addAnnotationExclusionFilter(String notAnnotation) { 243 Class<? extends Annotation> annotationClass = loadAnnotationClass(notAnnotation); 244 if (annotationClass != null) { 245 mFilter = mFilter.intersect(new AnnotationExclusionFilter(annotationClass)); 246 } 247 } 248 249 /** 250 * Build a request that will generate test started and test ended events, but will skip actual 251 * test execution. 252 */ 253 public void setSkipExecution(boolean b) { 254 mSkipExecution = b; 255 } 256 257 /** 258 * Builds the {@link TestRequest} based on current contents of added classes and methods. 259 * <p/> 260 * If no classes have been explicitly added, will scan the classpath for all tests. 261 * 262 */ 263 public TestRequest build(Instrumentation instr, Bundle bundle) { 264 if (mTestLoader.isEmpty()) { 265 // no class restrictions have been specified. Load all classes 266 loadClassesFromClassPath(); 267 } 268 269 Request request = classes(instr, bundle, mSkipExecution, new Computer(), 270 mTestLoader.getLoadedClasses().toArray(new Class[0])); 271 return new TestRequest(mTestLoader.getLoadFailures(), request.filterWith(mFilter)); 272 } 273 274 /** 275 * Create a <code>Request</code> that, when processed, will run all the tests 276 * in a set of classes. 277 * 278 * @param instr the {@link Instrumentation} to inject into any tests that require it 279 * @param bundle the {@link Bundle} of command line args to inject into any tests that require 280 * it 281 * @param computer Helps construct Runners from classes 282 * @param classes the classes containing the tests 283 * @return a <code>Request</code> that will cause all tests in the classes to be run 284 */ 285 private static Request classes(Instrumentation instr, Bundle bundle, boolean skipExecution, 286 Computer computer, Class<?>... classes) { 287 try { 288 AndroidRunnerBuilder builder = new AndroidRunnerBuilder(true, instr, bundle, 289 skipExecution); 290 Runner suite = computer.getSuite(builder, classes); 291 return Request.runner(suite); 292 } catch (InitializationError e) { 293 throw new RuntimeException( 294 "Suite constructor, called as above, should always complete"); 295 } 296 } 297 298 private void loadClassesFromClassPath() { 299 Collection<String> classNames = getClassNamesFromClassPath(); 300 for (String className : classNames) { 301 mTestLoader.loadIfTest(className); 302 } 303 } 304 305 private Collection<String> getClassNamesFromClassPath() { 306 Log.i(LOG_TAG, String.format("Scanning classpath to find tests in apks %s", 307 Arrays.toString(mApkPaths))); 308 ClassPathScanner scanner = new ClassPathScanner(mApkPaths); 309 try { 310 // exclude inner classes, and classes from junit and this lib namespace 311 return scanner.getClassPathEntries(new ChainedClassNameFilter( 312 new ExcludePackageNameFilter("junit"), 313 new ExcludePackageNameFilter("org.junit"), 314 new ExcludePackageNameFilter("org.hamcrest"), 315 new ExcludePackageNameFilter("org.mockito"), 316 new ExcludePackageNameFilter("com.android.dx"), 317 new ExcludePackageNameFilter("com.google.dexmaker"), 318 new ExternalClassNameFilter(), 319 new ExcludePackageNameFilter("com.android.test.runner.junit3"))); 320 } catch (IOException e) { 321 mWriter.println("failed to scan classes"); 322 Log.e(LOG_TAG, "Failed to scan classes", e); 323 } 324 return Collections.emptyList(); 325 } 326 327 /** 328 * Factory method for {@link ClassPathScanner}. 329 * <p/> 330 * Exposed so unit tests can mock. 331 */ 332 ClassPathScanner createClassPathScanner(String... apkPaths) { 333 return new ClassPathScanner(apkPaths); 334 } 335 336 @SuppressWarnings("unchecked") 337 private Class<? extends Annotation> loadAnnotationClass(String className) { 338 try { 339 Class<?> clazz = Class.forName(className); 340 return (Class<? extends Annotation>)clazz; 341 } catch (ClassNotFoundException e) { 342 Log.e(LOG_TAG, String.format("Could not find annotation class: %s", className)); 343 } catch (ClassCastException e) { 344 Log.e(LOG_TAG, String.format("Class %s is not an annotation", className)); 345 } 346 return null; 347 } 348 } 349