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