1 /*
2  * Copyright (C) 2008 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 android.test.suitebuilder;
18 
19 import android.content.Context;
20 import android.test.AndroidTestRunner;
21 import android.test.TestCaseUtil;
22 import android.util.Log;
23 import com.android.internal.util.Predicate;
24 import com.google.android.collect.Lists;
25 import static android.test.suitebuilder.TestGrouping.SORT_BY_FULLY_QUALIFIED_NAME;
26 import static android.test.suitebuilder.TestPredicates.REJECT_SUPPRESSED;
27 
28 import junit.framework.Test;
29 import junit.framework.TestCase;
30 import junit.framework.TestSuite;
31 
32 import java.util.List;
33 import java.util.Set;
34 import java.util.HashSet;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 
38 /**
39  * Build suites based on a combination of included packages, excluded packages,
40  * and predicates that must be satisfied.
41  */
42 public class TestSuiteBuilder {
43 
44     private Context context;
45     private final TestGrouping testGrouping = new TestGrouping(SORT_BY_FULLY_QUALIFIED_NAME);
46     private final Set<Predicate<TestMethod>> predicates = new HashSet<Predicate<TestMethod>>();
47     private List<TestCase> testCases;
48     private TestSuite rootSuite;
49     private TestSuite suiteForCurrentClass;
50     private String currentClassname;
51     private String suiteName;
52 
53     /**
54      * The given name is automatically prefixed with the package containing the tests to be run.
55      * If more than one package is specified, the first is used.
56      *
57      * @param clazz Use the class from your .apk. Use the class name for the test suite name.
58      *              Use the class' classloader in order to load classes for testing.
59      *              This is needed when running in the emulator.
60      */
TestSuiteBuilder(Class clazz)61     public TestSuiteBuilder(Class clazz) {
62         this(clazz.getName(), clazz.getClassLoader());
63     }
64 
TestSuiteBuilder(String name, ClassLoader classLoader)65     public TestSuiteBuilder(String name, ClassLoader classLoader) {
66         this.suiteName = name;
67         this.testGrouping.setClassLoader(classLoader);
68         this.testCases = Lists.newArrayList();
69         addRequirements(REJECT_SUPPRESSED);
70     }
71 
72     /** @hide pending API Council approval */
addTestClassByName(String testClassName, String testMethodName, Context context)73     public TestSuiteBuilder addTestClassByName(String testClassName, String testMethodName,
74             Context context) {
75 
76         AndroidTestRunner atr = new AndroidTestRunner();
77         atr.setContext(context);
78         atr.setTestClassName(testClassName, testMethodName);
79 
80         this.testCases.addAll(atr.getTestCases());
81         return this;
82     }
83 
84     /** @hide pending API Council approval */
addTestSuite(TestSuite testSuite)85     public TestSuiteBuilder addTestSuite(TestSuite testSuite) {
86         for (TestCase testCase : (List<TestCase>) TestCaseUtil.getTests(testSuite, true)) {
87             this.testCases.add(testCase);
88         }
89         return this;
90     }
91 
92     /**
93      * Include all tests that satisfy the requirements in the given packages and all sub-packages,
94      * unless otherwise specified.
95      *
96      * @param packageNames Names of packages to add.
97      * @return The builder for method chaining.
98      */
includePackages(String... packageNames)99     public TestSuiteBuilder includePackages(String... packageNames) {
100         testGrouping.addPackagesRecursive(packageNames);
101         return this;
102     }
103 
104     /**
105      * Exclude all tests in the given packages and all sub-packages, unless otherwise specified.
106      *
107      * @param packageNames Names of packages to remove.
108      * @return The builder for method chaining.
109      */
excludePackages(String... packageNames)110     public TestSuiteBuilder excludePackages(String... packageNames) {
111         testGrouping.removePackagesRecursive(packageNames);
112         return this;
113     }
114 
115     /**
116      * Exclude tests that fail to satisfy all of the given predicates.
117      *
118      * @param predicates Predicates to add to the list of requirements.
119      * @return The builder for method chaining.
120      */
addRequirements(List<Predicate<TestMethod>> predicates)121     public TestSuiteBuilder addRequirements(List<Predicate<TestMethod>> predicates) {
122         this.predicates.addAll(predicates);
123         return this;
124     }
125 
126     /**
127      * Include all junit tests that satisfy the requirements in the calling class' package and all
128      * sub-packages.
129      *
130      * @return The builder for method chaining.
131      */
includeAllPackagesUnderHere()132     public final TestSuiteBuilder includeAllPackagesUnderHere() {
133         StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
134 
135         String callingClassName = null;
136         String thisClassName = TestSuiteBuilder.class.getName();
137 
138         // We want to get the package of this method's calling class. This method's calling class
139         // should be one level below this class in the stack trace.
140         for (int i = 0; i < stackTraceElements.length; i++) {
141             StackTraceElement element = stackTraceElements[i];
142             if (thisClassName.equals(element.getClassName())
143                     && "includeAllPackagesUnderHere".equals(element.getMethodName())) {
144                 // We've found this class in the call stack. The calling class must be the
145                 // next class in the stack.
146                 callingClassName = stackTraceElements[i + 1].getClassName();
147                 break;
148             }
149         }
150 
151         String packageName = parsePackageNameFromClassName(callingClassName);
152         return includePackages(packageName);
153     }
154 
155     /**
156      * Override the default name for the suite being built. This should generally be called if you
157      * call {@link #addRequirements(com.android.internal.util.Predicate[])} to make it clear which
158      * tests will be included. The name you specify is automatically prefixed with the package
159      * containing the tests to be run. If more than one package is specified, the first is used.
160      *
161      * @param newSuiteName Prefix of name to give the suite being built.
162      * @return The builder for method chaining.
163      */
named(String newSuiteName)164     public TestSuiteBuilder named(String newSuiteName) {
165         suiteName = newSuiteName;
166         return this;
167     }
168 
169     /**
170      * Call this method once you've configured your builder as desired.
171      *
172      * @return The suite containing the requested tests.
173      */
build()174     public final TestSuite build() {
175         rootSuite = new TestSuite(getSuiteName());
176 
177         // Keep track of current class so we know when to create a new sub-suite.
178         currentClassname = null;
179         try {
180             for (TestMethod test : testGrouping.getTests()) {
181                 if (satisfiesAllPredicates(test)) {
182                     addTest(test);
183                 }
184             }
185             if (testCases.size() > 0) {
186                 for (TestCase testCase : testCases) {
187                     if (satisfiesAllPredicates(new TestMethod(testCase))) {
188                         addTest(testCase);
189                     }
190                 }
191             }
192         } catch (Exception exception) {
193             Log.i("TestSuiteBuilder", "Failed to create test.", exception);
194             TestSuite suite = new TestSuite(getSuiteName());
195             suite.addTest(new FailedToCreateTests(exception));
196             return suite;
197         }
198         return rootSuite;
199     }
200 
201     /**
202      * Subclasses use this method to determine the name of the suite.
203      *
204      * @return The package and suite name combined.
205      */
getSuiteName()206     protected String getSuiteName() {
207         return suiteName;
208     }
209 
210     /**
211      * Exclude tests that fail to satisfy all of the given predicates. If you call this method, you
212      * probably also want to call {@link #named(String)} to override the default suite name.
213      *
214      * @param predicates Predicates to add to the list of requirements.
215      * @return The builder for method chaining.
216      */
addRequirements(Predicate<TestMethod>.... predicates)217     public final TestSuiteBuilder addRequirements(Predicate<TestMethod>... predicates) {
218         ArrayList<Predicate<TestMethod>> list = new ArrayList<Predicate<TestMethod>>();
219         Collections.addAll(list, predicates);
220         return addRequirements(list);
221     }
222 
223     /**
224      * A special {@link junit.framework.TestCase} used to indicate a failure during the build()
225      * step.
226      */
227     public static class FailedToCreateTests extends TestCase {
228         private final Exception exception;
229 
FailedToCreateTests(Exception exception)230         public FailedToCreateTests(Exception exception) {
231             super("testSuiteConstructionFailed");
232             this.exception = exception;
233         }
234 
testSuiteConstructionFailed()235         public void testSuiteConstructionFailed() {
236             throw new RuntimeException("Exception during suite construction", exception);
237         }
238     }
239 
240     /**
241      * @return the test package that represents the packages that were included for our test suite.
242      *
243      * {@hide} Not needed for 1.0 SDK.
244      */
getTestGrouping()245     protected TestGrouping getTestGrouping() {
246         return testGrouping;
247     }
248 
satisfiesAllPredicates(TestMethod test)249     private boolean satisfiesAllPredicates(TestMethod test) {
250         for (Predicate<TestMethod> predicate : predicates) {
251             if (!predicate.apply(test)) {
252                 return false;
253             }
254         }
255         return true;
256     }
257 
addTest(TestMethod testMethod)258     private void addTest(TestMethod testMethod) throws Exception {
259         addSuiteIfNecessary(testMethod.getEnclosingClassname());
260         suiteForCurrentClass.addTest(testMethod.createTest());
261     }
262 
addTest(Test test)263     private void addTest(Test test) {
264         addSuiteIfNecessary(test.getClass().getName());
265         suiteForCurrentClass.addTest(test);
266     }
267 
addSuiteIfNecessary(String parentClassname)268     private void addSuiteIfNecessary(String parentClassname) {
269         if (!parentClassname.equals(currentClassname)) {
270             currentClassname = parentClassname;
271             suiteForCurrentClass = new TestSuite(parentClassname);
272             rootSuite.addTest(suiteForCurrentClass);
273         }
274     }
275 
parsePackageNameFromClassName(String className)276     private static String parsePackageNameFromClassName(String className) {
277         return className.substring(0, className.lastIndexOf('.'));
278     }
279 }
280