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
17package com.android.uiautomator.testrunner;
18
19import android.app.Activity;
20import android.app.IInstrumentationWatcher;
21import android.app.Instrumentation;
22import android.content.ComponentName;
23import android.os.Bundle;
24import android.os.Debug;
25import android.os.IBinder;
26import android.test.RepetitiveTest;
27import android.util.Log;
28
29import com.android.uiautomator.core.UiDevice;
30
31import junit.framework.AssertionFailedError;
32import junit.framework.Test;
33import junit.framework.TestCase;
34import junit.framework.TestListener;
35import junit.framework.TestResult;
36import junit.runner.BaseTestRunner;
37import junit.textui.ResultPrinter;
38
39import java.io.ByteArrayOutputStream;
40import java.io.PrintStream;
41import java.lang.Thread.UncaughtExceptionHandler;
42import java.lang.reflect.Method;
43import java.util.ArrayList;
44import java.util.List;
45
46/**
47 * @hide
48 */
49public class UiAutomatorTestRunner {
50
51    private static final String LOGTAG = UiAutomatorTestRunner.class.getSimpleName();
52    private static final int EXIT_OK = 0;
53    private static final int EXIT_EXCEPTION = -1;
54
55    private boolean mDebug;
56    private Bundle mParams = null;
57    private UiDevice mUiDevice;
58    private List<String> mTestClasses = null;
59    private FakeInstrumentationWatcher mWatcher = new FakeInstrumentationWatcher();
60    private IAutomationSupport mAutomationSupport = new IAutomationSupport() {
61        @Override
62        public void sendStatus(int resultCode, Bundle status) {
63            mWatcher.instrumentationStatus(null, resultCode, status);
64        }
65    };
66    private List<TestListener> mTestListeners = new ArrayList<TestListener>();
67
68    public void run(List<String> testClasses, Bundle params, boolean debug) {
69        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
70            @Override
71            public void uncaughtException(Thread thread, Throwable ex) {
72                Log.e(LOGTAG, "uncaught exception", ex);
73                Bundle results = new Bundle();
74                results.putString("shortMsg", ex.getClass().getName());
75                results.putString("longMsg", ex.getMessage());
76                mWatcher.instrumentationFinished(null, 0, results);
77                // bailing on uncaught exception
78                System.exit(EXIT_EXCEPTION);
79            }
80        });
81
82        mTestClasses = testClasses;
83        mParams = params;
84        mDebug = debug;
85        start();
86        System.exit(EXIT_OK);
87    }
88
89    /**
90     * Called after all test classes are in place, ready to test
91     */
92    protected void start() {
93        TestCaseCollector collector = getTestCaseCollector(this.getClass().getClassLoader());
94        try {
95            collector.addTestClasses(mTestClasses);
96        } catch (ClassNotFoundException e) {
97            // will be caught by uncaught handler
98            throw new RuntimeException(e.getMessage(), e);
99        }
100        if (mDebug) {
101            Debug.waitForDebugger();
102        }
103        mUiDevice = UiDevice.getInstance();
104        List<TestCase> testCases = collector.getTestCases();
105        Bundle testRunOutput = new Bundle();
106        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
107        PrintStream writer = new PrintStream(byteArrayOutputStream);
108        try {
109            StringResultPrinter resultPrinter = new StringResultPrinter(writer);
110
111            TestResult testRunResult = new TestResult();
112            // add test listeners
113            testRunResult.addListener(new WatcherResultPrinter(testCases.size()));
114            testRunResult.addListener(resultPrinter);
115            // add all custom listeners
116            for (TestListener listener : mTestListeners) {
117                testRunResult.addListener(listener);
118            }
119            long startTime = System.currentTimeMillis();
120
121            // run tests for realz!
122            for (TestCase testCase : testCases) {
123                prepareTestCase(testCase);
124                testCase.run(testRunResult);
125            }
126            long runTime = System.currentTimeMillis() - startTime;
127
128            resultPrinter.print2(testRunResult, runTime);
129        } catch (Throwable t) {
130            // catch all exceptions so a more verbose error message can be outputted
131            writer.println(String.format("Test run aborted due to unexpected exception: %s",
132                            t.getMessage()));
133            t.printStackTrace(writer);
134        } finally {
135            testRunOutput.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
136                    String.format("\nTest results for %s=%s",
137                    getClass().getSimpleName(),
138                    byteArrayOutputStream.toString()));
139            writer.close();
140            mAutomationSupport.sendStatus(Activity.RESULT_OK, testRunOutput);
141        }
142    }
143
144    // copy & pasted from com.android.commands.am.Am
145    private class FakeInstrumentationWatcher implements IInstrumentationWatcher {
146
147        private boolean mRawMode = true;
148
149        @Override
150        public IBinder asBinder() {
151            throw new UnsupportedOperationException("I'm just a fake!");
152        }
153
154        @Override
155        public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) {
156            synchronized (this) {
157                // pretty printer mode?
158                String pretty = null;
159                if (!mRawMode && results != null) {
160                    pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
161                }
162                if (pretty != null) {
163                    System.out.print(pretty);
164                } else {
165                    if (results != null) {
166                        for (String key : results.keySet()) {
167                            System.out.println("INSTRUMENTATION_STATUS: " + key + "="
168                                    + results.get(key));
169                        }
170                    }
171                    System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode);
172                }
173                notifyAll();
174            }
175        }
176
177        @Override
178        public void instrumentationFinished(ComponentName name, int resultCode, Bundle results) {
179            synchronized (this) {
180                // pretty printer mode?
181                String pretty = null;
182                if (!mRawMode && results != null) {
183                    pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
184                }
185                if (pretty != null) {
186                    System.out.println(pretty);
187                } else {
188                    if (results != null) {
189                        for (String key : results.keySet()) {
190                            System.out.println("INSTRUMENTATION_RESULT: " + key + "="
191                                    + results.get(key));
192                        }
193                    }
194                    System.out.println("INSTRUMENTATION_CODE: " + resultCode);
195                }
196                notifyAll();
197            }
198        }
199    }
200
201    // Copy & pasted from InstrumentationTestRunner.WatcherResultPrinter
202    private class WatcherResultPrinter implements TestListener {
203
204        private static final String REPORT_KEY_NUM_TOTAL = "numtests";
205        private static final String REPORT_KEY_NAME_CLASS = "class";
206        private static final String REPORT_KEY_NUM_CURRENT = "current";
207        private static final String REPORT_KEY_NAME_TEST = "test";
208        private static final String REPORT_KEY_NUM_ITERATIONS = "numiterations";
209        private static final String REPORT_VALUE_ID = "UiAutomatorTestRunner";
210        private static final String REPORT_KEY_STACK = "stack";
211
212        private static final int REPORT_VALUE_RESULT_START = 1;
213        private static final int REPORT_VALUE_RESULT_ERROR = -1;
214        private static final int REPORT_VALUE_RESULT_FAILURE = -2;
215
216        private final Bundle mResultTemplate;
217        Bundle mTestResult;
218        int mTestNum = 0;
219        int mTestResultCode = 0;
220        String mTestClass = null;
221
222        public WatcherResultPrinter(int numTests) {
223            mResultTemplate = new Bundle();
224            mResultTemplate.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID);
225            mResultTemplate.putInt(REPORT_KEY_NUM_TOTAL, numTests);
226        }
227
228        /**
229         * send a status for the start of a each test, so long tests can be seen
230         * as "running"
231         */
232        @Override
233        public void startTest(Test test) {
234            String testClass = test.getClass().getName();
235            String testName = ((TestCase) test).getName();
236            mTestResult = new Bundle(mResultTemplate);
237            mTestResult.putString(REPORT_KEY_NAME_CLASS, testClass);
238            mTestResult.putString(REPORT_KEY_NAME_TEST, testName);
239            mTestResult.putInt(REPORT_KEY_NUM_CURRENT, ++mTestNum);
240            // pretty printing
241            if (testClass != null && !testClass.equals(mTestClass)) {
242                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
243                        String.format("\n%s:", testClass));
244                mTestClass = testClass;
245            } else {
246                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "");
247            }
248
249            Method testMethod = null;
250            try {
251                testMethod = test.getClass().getMethod(testName);
252                // Report total number of iterations, if test is repetitive
253                if (testMethod.isAnnotationPresent(RepetitiveTest.class)) {
254                    int numIterations = testMethod.getAnnotation(RepetitiveTest.class)
255                            .numIterations();
256                    mTestResult.putInt(REPORT_KEY_NUM_ITERATIONS, numIterations);
257                }
258            } catch (NoSuchMethodException e) {
259                // ignore- the test with given name does not exist. Will be
260                // handled during test
261                // execution
262            }
263
264            mAutomationSupport.sendStatus(REPORT_VALUE_RESULT_START, mTestResult);
265            mTestResultCode = 0;
266        }
267
268        @Override
269        public void addError(Test test, Throwable t) {
270            mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t));
271            mTestResultCode = REPORT_VALUE_RESULT_ERROR;
272            // pretty printing
273            mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
274                String.format("\nError in %s:\n%s",
275                    ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t)));
276        }
277
278        @Override
279        public void addFailure(Test test, AssertionFailedError t) {
280            mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t));
281            mTestResultCode = REPORT_VALUE_RESULT_FAILURE;
282            // pretty printing
283            mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
284                String.format("\nFailure in %s:\n%s",
285                    ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t)));
286        }
287
288        @Override
289        public void endTest(Test test) {
290            if (mTestResultCode == 0) {
291                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, ".");
292            }
293            mAutomationSupport.sendStatus(mTestResultCode, mTestResult);
294        }
295
296    }
297
298    // copy pasted from InstrumentationTestRunner
299    private class StringResultPrinter extends ResultPrinter {
300
301        public StringResultPrinter(PrintStream writer) {
302            super(writer);
303        }
304
305        synchronized void print2(TestResult result, long runTime) {
306            printHeader(runTime);
307            printFooter(result);
308        }
309    }
310
311    protected TestCaseCollector getTestCaseCollector(ClassLoader classLoader) {
312        return new TestCaseCollector(classLoader, new UiAutomatorTestCaseFilter());
313    }
314
315    protected void addTestListener(TestListener listener) {
316        if (!mTestListeners.contains(listener)) {
317            mTestListeners.add(listener);
318        }
319    }
320
321    protected void removeTestListener(TestListener listener) {
322        mTestListeners.remove(listener);
323    }
324
325    /**
326     * subclass may override this method to perform further preparation
327     *
328     * @param testCase
329     */
330    protected void prepareTestCase(TestCase testCase) {
331        ((UiAutomatorTestCase)testCase).setAutomationSupport(mAutomationSupport);
332        ((UiAutomatorTestCase)testCase).setUiDevice(mUiDevice);
333        ((UiAutomatorTestCase)testCase).setParams(mParams);
334    }
335}
336