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 */ 16package com.android.test.runner; 17 18import android.app.Instrumentation; 19import android.test.suitebuilder.annotation.LargeTest; 20import android.test.suitebuilder.annotation.MediumTest; 21import android.test.suitebuilder.annotation.SmallTest; 22import android.test.suitebuilder.annotation.Suppress; 23import android.util.Log; 24 25import com.android.test.runner.ClassPathScanner.ChainedClassNameFilter; 26import com.android.test.runner.ClassPathScanner.ExcludePackageNameFilter; 27import com.android.test.runner.ClassPathScanner.ExternalClassNameFilter; 28 29import org.junit.runner.Computer; 30import org.junit.runner.Description; 31import org.junit.runner.Request; 32import org.junit.runner.Runner; 33import org.junit.runner.manipulation.Filter; 34import org.junit.runners.model.InitializationError; 35 36import java.io.IOException; 37import java.io.PrintStream; 38import java.lang.annotation.Annotation; 39import java.util.Arrays; 40import java.util.Collection; 41import java.util.Collections; 42 43/** 44 * Builds a {@link Request} from test classes in given apk paths, filtered on provided set of 45 * restrictions. 46 */ 47public class TestRequestBuilder { 48 49 private static final String LOG_TAG = "TestRequestBuilder"; 50 51 private String[] mApkPaths; 52 private TestLoader mTestLoader; 53 private Filter mFilter = new AnnotationExclusionFilter(Suppress.class); 54 private PrintStream mWriter; 55 private boolean mSkipExecution = false; 56 57 /** 58 * Filter that only runs tests whose method or class has been annotated with given filter. 59 */ 60 private static class AnnotationInclusionFilter extends Filter { 61 62 private final Class<? extends Annotation> mAnnotationClass; 63 64 AnnotationInclusionFilter(Class<? extends Annotation> annotation) { 65 mAnnotationClass = annotation; 66 } 67 68 /** 69 * {@inheritDoc} 70 */ 71 @Override 72 public boolean shouldRun(Description description) { 73 if (description.isTest()) { 74 return description.getAnnotation(mAnnotationClass) != null || 75 description.getTestClass().isAnnotationPresent(mAnnotationClass); 76 } else { 77 // don't filter out any test classes/suites, because their methods may have correct 78 // annotation 79 return true; 80 } 81 } 82 83 /** 84 * {@inheritDoc} 85 */ 86 @Override 87 public String describe() { 88 return String.format("annotation %s", mAnnotationClass.getName()); 89 } 90 } 91 92 /** 93 * Filter out tests whose method or class has been annotated with given filter. 94 */ 95 private static class AnnotationExclusionFilter extends Filter { 96 97 private final Class<? extends Annotation> mAnnotationClass; 98 99 AnnotationExclusionFilter(Class<? extends Annotation> annotation) { 100 mAnnotationClass = annotation; 101 } 102 103 /** 104 * {@inheritDoc} 105 */ 106 @Override 107 public boolean shouldRun(Description description) { 108 if (description.getTestClass().isAnnotationPresent(mAnnotationClass) || 109 description.getAnnotation(mAnnotationClass) != null) { 110 return false; 111 } else { 112 return true; 113 } 114 } 115 116 /** 117 * {@inheritDoc} 118 */ 119 @Override 120 public String describe() { 121 return String.format("not annotation %s", mAnnotationClass.getName()); 122 } 123 } 124 125 public TestRequestBuilder(PrintStream writer, String... apkPaths) { 126 mApkPaths = apkPaths; 127 mTestLoader = new TestLoader(writer); 128 } 129 130 /** 131 * Add a test class to be executed. All test methods in this class will be executed. 132 * 133 * @param className 134 */ 135 public void addTestClass(String className) { 136 mTestLoader.loadClass(className); 137 } 138 139 /** 140 * Adds a test method to run. 141 * <p/> 142 * Currently only supports one test method to be run. 143 */ 144 public void addTestMethod(String testClassName, String testMethodName) { 145 Class<?> clazz = mTestLoader.loadClass(testClassName); 146 if (clazz != null) { 147 mFilter = mFilter.intersect(Filter.matchMethodDescription( 148 Description.createTestDescription(clazz, testMethodName))); 149 } 150 } 151 152 /** 153 * Run only tests with given size 154 * @param testSize 155 */ 156 public void addTestSizeFilter(String testSize) { 157 if ("small".equals(testSize)) { 158 mFilter = mFilter.intersect(new AnnotationInclusionFilter(SmallTest.class)); 159 } else if ("medium".equals(testSize)) { 160 mFilter = mFilter.intersect(new AnnotationInclusionFilter(MediumTest.class)); 161 } else if ("large".equals(testSize)) { 162 mFilter = mFilter.intersect(new AnnotationInclusionFilter(LargeTest.class)); 163 } else { 164 Log.e(LOG_TAG, String.format("Unrecognized test size '%s'", testSize)); 165 } 166 } 167 168 /** 169 * Only run tests annotated with given annotation class. 170 * 171 * @param annotation the full class name of annotation 172 */ 173 public void addAnnotationInclusionFilter(String annotation) { 174 Class<? extends Annotation> annotationClass = loadAnnotationClass(annotation); 175 if (annotationClass != null) { 176 mFilter = mFilter.intersect(new AnnotationInclusionFilter(annotationClass)); 177 } 178 } 179 180 /** 181 * Skip tests annotated with given annotation class. 182 * 183 * @param notAnnotation the full class name of annotation 184 */ 185 public void addAnnotationExclusionFilter(String notAnnotation) { 186 Class<? extends Annotation> annotationClass = loadAnnotationClass(notAnnotation); 187 if (annotationClass != null) { 188 mFilter = mFilter.intersect(new AnnotationExclusionFilter(annotationClass)); 189 } 190 } 191 192 /** 193 * Build a request that will generate test started and test ended events, but will skip actual 194 * test execution. 195 */ 196 public void setSkipExecution(boolean b) { 197 mSkipExecution = b; 198 } 199 200 /** 201 * Builds the {@link TestRequest} based on current contents of added classes and methods. 202 * <p/> 203 * If no classes have been explicitly added, will scan the classpath for all tests. 204 * 205 */ 206 public TestRequest build(Instrumentation instr) { 207 if (mTestLoader.isEmpty()) { 208 // no class restrictions have been specified. Load all classes 209 loadClassesFromClassPath(); 210 } 211 212 Request request = classes(instr, mSkipExecution, new Computer(), 213 mTestLoader.getLoadedClasses().toArray(new Class[0])); 214 return new TestRequest(mTestLoader.getLoadFailures(), request.filterWith(mFilter)); 215 } 216 217 /** 218 * Create a <code>Request</code> that, when processed, will run all the tests 219 * in a set of classes. 220 * 221 * @param instr the {@link Instrumentation} to inject into any tests that require it 222 * @param computer Helps construct Runners from classes 223 * @param classes the classes containing the tests 224 * @return a <code>Request</code> that will cause all tests in the classes to be run 225 */ 226 private static Request classes(Instrumentation instr, boolean skipExecution, 227 Computer computer, Class<?>... classes) { 228 try { 229 AndroidRunnerBuilder builder = new AndroidRunnerBuilder(true, instr, skipExecution); 230 Runner suite = computer.getSuite(builder, classes); 231 return Request.runner(suite); 232 } catch (InitializationError e) { 233 throw new RuntimeException( 234 "Suite constructor, called as above, should always complete"); 235 } 236 } 237 238 private void loadClassesFromClassPath() { 239 Collection<String> classNames = getClassNamesFromClassPath(); 240 for (String className : classNames) { 241 mTestLoader.loadIfTest(className); 242 } 243 } 244 245 private Collection<String> getClassNamesFromClassPath() { 246 Log.i(LOG_TAG, String.format("Scanning classpath to find tests in apks %s", 247 Arrays.toString(mApkPaths))); 248 ClassPathScanner scanner = new ClassPathScanner(mApkPaths); 249 try { 250 // exclude inner classes, and classes from junit and this lib namespace 251 return scanner.getClassPathEntries(new ChainedClassNameFilter( 252 new ExcludePackageNameFilter("junit"), 253 new ExcludePackageNameFilter("org.junit"), 254 new ExcludePackageNameFilter("org.hamcrest"), 255 new ExternalClassNameFilter(), 256 new ExcludePackageNameFilter("com.android.test.runner.junit3"))); 257 } catch (IOException e) { 258 mWriter.println("failed to scan classes"); 259 Log.e(LOG_TAG, "Failed to scan classes", e); 260 } 261 return Collections.emptyList(); 262 } 263 264 /** 265 * Factory method for {@link ClassPathScanner}. 266 * <p/> 267 * Exposed so unit tests can mock. 268 */ 269 ClassPathScanner createClassPathScanner(String... apkPaths) { 270 return new ClassPathScanner(apkPaths); 271 } 272 273 @SuppressWarnings("unchecked") 274 private Class<? extends Annotation> loadAnnotationClass(String className) { 275 try { 276 Class<?> clazz = Class.forName(className); 277 return (Class<? extends Annotation>)clazz; 278 } catch (ClassNotFoundException e) { 279 Log.e(LOG_TAG, String.format("Could not find annotation class: %s", className)); 280 } catch (ClassCastException e) { 281 Log.e(LOG_TAG, String.format("Class %s is not an annotation", className)); 282 } 283 return null; 284 } 285} 286