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