1 /*
2  * Copyright (C) 2015 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 com.android.cts.core.runner;
18 
19 import android.app.Activity;
20 import android.app.Instrumentation;
21 import android.os.Bundle;
22 import android.os.Debug;
23 import android.support.test.internal.runner.listener.InstrumentationResultPrinter;
24 import android.support.test.internal.runner.listener.InstrumentationRunListener;
25 import android.support.test.internal.util.AndroidRunnerParams;
26 import android.util.Log;
27 import com.android.cts.core.runner.support.ExtendedAndroidRunnerBuilder;
28 import com.google.common.base.Splitter;
29 import java.io.BufferedReader;
30 import java.io.ByteArrayOutputStream;
31 import java.io.FileReader;
32 import java.io.IOException;
33 import java.io.PrintStream;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Set;
41 import org.junit.runner.Computer;
42 import org.junit.runner.JUnitCore;
43 import org.junit.runner.Request;
44 import org.junit.runner.Result;
45 import org.junit.runner.Runner;
46 import org.junit.runner.manipulation.Filter;
47 import org.junit.runner.manipulation.Filterable;
48 import org.junit.runner.manipulation.NoTestsRemainException;
49 import org.junit.runner.notification.RunListener;
50 import org.junit.runners.model.InitializationError;
51 import org.junit.runners.model.RunnerBuilder;
52 
53 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_COUNT;
54 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_DEBUG;
55 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_LOG_ONLY;
56 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_NOT_TEST_CLASS;
57 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_NOT_TEST_FILE;
58 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_NOT_TEST_PACKAGE;
59 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_TEST_CLASS;
60 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_TEST_FILE;
61 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_TEST_PACKAGE;
62 import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_TIMEOUT;
63 
64 /**
65  * A drop-in replacement for AndroidJUnitTestRunner, which understands the same arguments, and has
66  * similar functionality, but can filter by expectations and allows a custom runner-builder to be
67  * provided.
68  */
69 public class CoreTestRunner extends Instrumentation {
70 
71     static final String TAG = "LibcoreTestRunner";
72 
73     private static final java.lang.String ARGUMENT_ROOT_CLASSES = "core-root-classes";
74 
75     private static final String ARGUMENT_CORE_LISTENER = "core-listener";
76 
77     private static final Splitter CLASS_LIST_SPLITTER = Splitter.on(',').trimResults();
78 
79     /** The args for the runner. */
80     private Bundle args;
81 
82     /** Only log the number and names of tests, and not run them. */
83     private boolean logOnly;
84 
85     /** The amount of time in millis to wait for a single test to complete. */
86     private long testTimeout;
87 
88     /**
89      * The list of tests to run.
90      */
91     private TestList testList;
92 
93     /**
94      * The list of {@link RunListener} classes to create.
95      */
96     private List<Class<? extends RunListener>> listenerClasses;
97     private Filter expectationFilter;
98 
99     @Override
onCreate(final Bundle args)100     public void onCreate(final Bundle args) {
101         super.onCreate(args);
102         this.args = args;
103 
104         boolean debug = "true".equalsIgnoreCase(args.getString(ARGUMENT_DEBUG));
105         if (debug) {
106             Log.i(TAG, "Waiting for debugger to connect...");
107             Debug.waitForDebugger();
108             Log.i(TAG, "Debugger connected.");
109         }
110 
111         // Log the message only after getting a value from the args so that the args are
112         // unparceled.
113         Log.d(TAG, "In OnCreate: " + args);
114 
115         // Treat logOnly and count as the same. This is not quite true as count should only send
116         // the host the number of tests but logOnly should send the name and number. However,
117         // this is how this has always behaved and it does not appear to have caused any problems.
118         // Changing it seems unnecessary given that count is CTSv1 only and CTSv1 will be removed
119         // soon now that CTSv2 is ready.
120         boolean testCountOnly = args.getBoolean(ARGUMENT_COUNT);
121         this.logOnly = "true".equalsIgnoreCase(args.getString(ARGUMENT_LOG_ONLY)) || testCountOnly;
122         this.testTimeout = parseUnsignedLong(args.getString(ARGUMENT_TIMEOUT), ARGUMENT_TIMEOUT);
123 
124         expectationFilter = new ExpectationBasedFilter(args);
125 
126         // The test can be run specifying a list of tests to run, or as cts-tradefed does it,
127         // by passing a fileName with a test to run on each line.
128         Set<String> testNameSet = new HashSet<>();
129         String arg;
130         if ((arg = args.getString(ARGUMENT_TEST_FILE)) != null) {
131             // The tests are specified in a file.
132             try {
133                 testNameSet.addAll(readTestsFromFile(arg));
134             } catch (IOException err) {
135                 finish(Activity.RESULT_CANCELED, new Bundle());
136                 return;
137             }
138         } else if ((arg = args.getString(ARGUMENT_TEST_CLASS)) != null) {
139             // The tests are specified in a String passed in the bundle.
140             String[] tests = arg.split(",");
141             testNameSet.addAll(Arrays.asList(tests));
142         }
143 
144         // Tests may be excluded from the run by passing a list of tests not to run,
145         // or by passing a fileName with a test not to run on each line.
146         Set<String> notTestNameSet = new HashSet<>();
147         if ((arg = args.getString(ARGUMENT_NOT_TEST_FILE)) != null) {
148             // The tests are specified in a file.
149             try {
150                 notTestNameSet.addAll(readTestsFromFile(arg));
151             } catch (IOException err) {
152                 finish(Activity.RESULT_CANCELED, new Bundle());
153                 return;
154             }
155         } else if ((arg = args.getString(ARGUMENT_NOT_TEST_CLASS)) != null) {
156             // The classes are specified in a String passed in the bundle
157             String[] tests = arg.split(",");
158             notTestNameSet.addAll(Arrays.asList(tests));
159         }
160 
161         Set<String> packageNameSet = new HashSet<>();
162         if ((arg = args.getString(ARGUMENT_TEST_PACKAGE)) != null) {
163             // The packages are specified in a String passed in the bundle
164             String[] packages = arg.split(",");
165             packageNameSet.addAll(Arrays.asList(packages));
166         }
167 
168         Set<String> notPackageNameSet = new HashSet<>();
169         if ((arg = args.getString(ARGUMENT_NOT_TEST_PACKAGE)) != null) {
170             // The packages are specified in a String passed in the bundle
171             String[] packages = arg.split(",");
172             notPackageNameSet.addAll(Arrays.asList(packages));
173         }
174 
175         List<String> roots = getRootClassNames(args);
176         if (roots == null) {
177             // Find all test classes
178             Collection<Class<?>> classes = TestClassFinder.getClasses(
179                 Collections.singletonList(getContext().getPackageCodePath()),
180                 getClass().getClassLoader());
181             testList = new TestList(classes);
182         } else {
183             testList = TestList.rootList(roots);
184         }
185 
186         testList.addIncludeTestPackages(packageNameSet);
187         testList.addExcludeTestPackages(notPackageNameSet);
188         testList.addIncludeTests(testNameSet);
189         testList.addExcludeTests(notTestNameSet);
190 
191         listenerClasses = new ArrayList<>();
192         String listenerArg = args.getString(ARGUMENT_CORE_LISTENER);
193         if (listenerArg != null) {
194             List<String> listenerClassNames = CLASS_LIST_SPLITTER.splitToList(listenerArg);
195             for (String listenerClassName : listenerClassNames) {
196                 try {
197                     Class<? extends RunListener> listenerClass = Class.forName(listenerClassName)
198                             .asSubclass(RunListener.class);
199                     listenerClasses.add(listenerClass);
200                 } catch (ClassNotFoundException e) {
201                     Log.e(TAG, "Could not load listener class: " + listenerClassName, e);
202                 }
203             }
204         }
205 
206         start();
207     }
208 
getRootClassNames(Bundle args)209     private List<String> getRootClassNames(Bundle args) {
210         String rootClasses = args.getString(ARGUMENT_ROOT_CLASSES);
211         List<String> roots;
212         if (rootClasses == null) {
213             roots = null;
214         } else {
215             roots = CLASS_LIST_SPLITTER.splitToList(rootClasses);
216         }
217         return roots;
218     }
219 
220     @Override
onStart()221     public void onStart() {
222         if (logOnly) {
223             Log.d(TAG, "Counting/logging tests only");
224         } else {
225             Log.d(TAG, "Running tests");
226         }
227 
228         AndroidRunnerParams runnerParams = new AndroidRunnerParams(this, args,
229                 false, testTimeout, false /*ignoreSuiteMethods*/);
230 
231         Runner runner;
232         try {
233             RunnerBuilder runnerBuilder = new ExtendedAndroidRunnerBuilder(runnerParams);
234             Class[] classes = testList.getClassesToRun();
235             for (Class cls : classes) {
236               Log.d(TAG, "Found class to run: " + cls.getName());
237             }
238             runner = new Computer().getSuite(runnerBuilder, classes);
239 
240             if (runner instanceof Filterable) {
241                 Log.d(TAG, "Applying filters");
242                 Filterable filterable = (Filterable) runner;
243 
244                 // Filter out all the tests that are expected to fail.
245                 try {
246                     filterable.filter(expectationFilter);
247                 } catch (NoTestsRemainException e) {
248                     // Sometimes filtering will remove all tests but we do not care about that.
249                 }
250                 Log.d(TAG, "Applied filters");
251             }
252 
253             // If the tests are only supposed to be logged and not actually run then replace the
254             // runner with a runner that will fire notifications for all the tests that would have
255             // been run. This is needed because CTSv2 does a log only run through a CTS module in
256             // order to generate a list of tests that will be run so that it can monitor them.
257             // Encapsulating that in a Runner implementation makes it easier to leverage the
258             // existing code for running tests.
259             if (logOnly) {
260                 runner = new DescriptionHierarchyNotifier(runner.getDescription());
261             }
262 
263         } catch (InitializationError e) {
264             throw new RuntimeException("Could not create a suite", e);
265         }
266 
267         InstrumentationResultPrinter instrumentationResultPrinter =
268                 new InstrumentationResultPrinter();
269         instrumentationResultPrinter.setInstrumentation(this);
270 
271         JUnitCore core = new JUnitCore();
272         core.addListener(instrumentationResultPrinter);
273 
274         // If not logging the list of tests then add any additional configured listeners. These
275         // must be added before firing any events.
276         if (!logOnly) {
277             // Add additional configured listeners.
278             for (Class<? extends RunListener> listenerClass : listenerClasses) {
279                 try {
280                     RunListener runListener = listenerClass.newInstance();
281                     if (runListener instanceof InstrumentationRunListener) {
282                         ((InstrumentationRunListener) runListener).setInstrumentation(this);
283                     }
284                     core.addListener(runListener);
285                 } catch (InstantiationException | IllegalAccessException e) {
286                     Log.e(TAG,
287                             "Could not create instance of listener: " + listenerClass, e);
288                 }
289             }
290         }
291 
292         Log.d(TAG, "Finished preparations, running/listing tests");
293 
294         Bundle results = new Bundle();
295         Result junitResults = new Result();
296         try {
297             junitResults = core.run(Request.runner(runner));
298         } catch (RuntimeException e) {
299             final String msg = "Fatal exception when running tests";
300             Log.e(TAG, msg, e);
301             // report the exception to instrumentation out
302             results.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
303                     msg + "\n" + Log.getStackTraceString(e));
304         } finally {
305             ByteArrayOutputStream summaryStream = new ByteArrayOutputStream();
306             // create the stream used to output summary data to the user
307             PrintStream summaryWriter = new PrintStream(summaryStream);
308             instrumentationResultPrinter.instrumentationRunFinished(summaryWriter,
309                     results, junitResults);
310             summaryWriter.close();
311             results.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
312                     String.format("\n%s", summaryStream.toString()));
313         }
314 
315 
316         Log.d(TAG, "Finished");
317         finish(Activity.RESULT_OK, results);
318     }
319 
320     /**
321      * Read tests from a specified file.
322      *
323      * @return class names of tests. If there was an error reading the file, null is returned.
324      */
readTestsFromFile(String fileName)325     private static List<String> readTestsFromFile(String fileName) throws IOException {
326         try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
327             List<String> tests = new ArrayList<>();
328             String line;
329             while ((line = br.readLine()) != null) {
330                 tests.add(line);
331             }
332             return tests;
333         } catch (IOException err) {
334             Log.e(TAG, "There was an error reading the test class list: " + err.getMessage());
335             throw err;
336         }
337     }
338 
339     /**
340      * Parse long from given value - except either Long or String.
341      *
342      * @return the value, -1 if not found
343      * @throws NumberFormatException if value is negative or not a number
344      */
parseUnsignedLong(Object value, String name)345     private static long parseUnsignedLong(Object value, String name) {
346         if (value != null) {
347             long longValue = Long.parseLong(value.toString());
348             if (longValue < 0) {
349                 throw new NumberFormatException(name + " can not be negative");
350             }
351             return longValue;
352         }
353         return -1;
354     }
355 }
356