/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.uiautomator.testrunner; import android.app.Activity; import android.app.IInstrumentationWatcher; import android.app.Instrumentation; import android.content.ComponentName; import android.os.Bundle; import android.os.Debug; import android.os.HandlerThread; import android.os.IBinder; import android.os.SystemClock; import android.test.RepetitiveTest; import android.util.Log; import com.android.uiautomator.core.ShellUiAutomatorBridge; import com.android.uiautomator.core.Tracer; import com.android.uiautomator.core.UiAutomationShellWrapper; import com.android.uiautomator.core.UiDevice; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.lang.Thread.UncaughtExceptionHandler; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestListener; import junit.framework.TestResult; import junit.runner.BaseTestRunner; import junit.textui.ResultPrinter; /** * @hide */ public class UiAutomatorTestRunner { private static final String LOGTAG = UiAutomatorTestRunner.class.getSimpleName(); private static final int EXIT_OK = 0; private static final int EXIT_EXCEPTION = -1; private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread"; private boolean mDebug; private boolean mMonkey; private Bundle mParams = null; private UiDevice mUiDevice; private List mTestClasses = null; private final FakeInstrumentationWatcher mWatcher = new FakeInstrumentationWatcher(); private final IAutomationSupport mAutomationSupport = new IAutomationSupport() { @Override public void sendStatus(int resultCode, Bundle status) { mWatcher.instrumentationStatus(null, resultCode, status); } }; private final List mTestListeners = new ArrayList(); private HandlerThread mHandlerThread; public void run(List testClasses, Bundle params, boolean debug, boolean monkey) { Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { Log.e(LOGTAG, "uncaught exception", ex); Bundle results = new Bundle(); results.putString("shortMsg", ex.getClass().getName()); results.putString("longMsg", ex.getMessage()); mWatcher.instrumentationFinished(null, 0, results); // bailing on uncaught exception System.exit(EXIT_EXCEPTION); } }); mTestClasses = testClasses; mParams = params; mDebug = debug; mMonkey = monkey; start(); System.exit(EXIT_OK); } /** * Called after all test classes are in place, ready to test */ protected void start() { TestCaseCollector collector = getTestCaseCollector(this.getClass().getClassLoader()); try { collector.addTestClasses(mTestClasses); } catch (ClassNotFoundException e) { // will be caught by uncaught handler throw new RuntimeException(e.getMessage(), e); } if (mDebug) { Debug.waitForDebugger(); } mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); mHandlerThread.setDaemon(true); mHandlerThread.start(); UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper(); automationWrapper.connect(); long startTime = SystemClock.uptimeMillis(); TestResult testRunResult = new TestResult(); ResultReporter resultPrinter; String outputFormat = mParams.getString("outputFormat"); List testCases = collector.getTestCases(); Bundle testRunOutput = new Bundle(); if ("simple".equals(outputFormat)) { resultPrinter = new SimpleResultPrinter(System.out, true); } else { resultPrinter = new WatcherResultPrinter(testCases.size()); } try { automationWrapper.setRunAsMonkey(mMonkey); mUiDevice = UiDevice.getInstance(); mUiDevice.initialize(new ShellUiAutomatorBridge(automationWrapper.getUiAutomation())); String traceType = mParams.getString("traceOutputMode"); if(traceType != null) { Tracer.Mode mode = Tracer.Mode.valueOf(Tracer.Mode.class, traceType); if (mode == Tracer.Mode.FILE || mode == Tracer.Mode.ALL) { String filename = mParams.getString("traceLogFilename"); if (filename == null) { throw new RuntimeException("Name of log file not specified. " + "Please specify it using traceLogFilename parameter"); } Tracer.getInstance().setOutputFilename(filename); } Tracer.getInstance().setOutputMode(mode); } // add test listeners testRunResult.addListener(resultPrinter); // add all custom listeners for (TestListener listener : mTestListeners) { testRunResult.addListener(listener); } // run tests for realz! for (TestCase testCase : testCases) { prepareTestCase(testCase); testCase.run(testRunResult); } } catch (Throwable t) { // catch all exceptions so a more verbose error message can be outputted resultPrinter.printUnexpectedError(t); } finally { long runTime = SystemClock.uptimeMillis() - startTime; resultPrinter.print(testRunResult, runTime, testRunOutput); automationWrapper.disconnect(); automationWrapper.setRunAsMonkey(false); mHandlerThread.quit(); } } // copy & pasted from com.android.commands.am.Am private class FakeInstrumentationWatcher implements IInstrumentationWatcher { private final boolean mRawMode = true; @Override public IBinder asBinder() { throw new UnsupportedOperationException("I'm just a fake!"); } @Override public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) { synchronized (this) { // pretty printer mode? String pretty = null; if (!mRawMode && results != null) { pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT); } if (pretty != null) { System.out.print(pretty); } else { if (results != null) { for (String key : results.keySet()) { System.out.println("INSTRUMENTATION_STATUS: " + key + "=" + results.get(key)); } } System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode); } notifyAll(); } } @Override public void instrumentationFinished(ComponentName name, int resultCode, Bundle results) { synchronized (this) { // pretty printer mode? String pretty = null; if (!mRawMode && results != null) { pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT); } if (pretty != null) { System.out.println(pretty); } else { if (results != null) { for (String key : results.keySet()) { System.out.println("INSTRUMENTATION_RESULT: " + key + "=" + results.get(key)); } } System.out.println("INSTRUMENTATION_CODE: " + resultCode); } notifyAll(); } } } private interface ResultReporter extends TestListener { public void print(TestResult result, long runTime, Bundle testOutput); public void printUnexpectedError(Throwable t); } // Copy & pasted from InstrumentationTestRunner.WatcherResultPrinter private class WatcherResultPrinter implements ResultReporter { private static final String REPORT_KEY_NUM_TOTAL = "numtests"; private static final String REPORT_KEY_NAME_CLASS = "class"; private static final String REPORT_KEY_NUM_CURRENT = "current"; private static final String REPORT_KEY_NAME_TEST = "test"; private static final String REPORT_KEY_NUM_ITERATIONS = "numiterations"; private static final String REPORT_VALUE_ID = "UiAutomatorTestRunner"; private static final String REPORT_KEY_STACK = "stack"; private static final int REPORT_VALUE_RESULT_START = 1; private static final int REPORT_VALUE_RESULT_ERROR = -1; private static final int REPORT_VALUE_RESULT_FAILURE = -2; private final Bundle mResultTemplate; Bundle mTestResult; int mTestNum = 0; int mTestResultCode = 0; String mTestClass = null; private final SimpleResultPrinter mPrinter; private final ByteArrayOutputStream mStream; private final PrintStream mWriter; public WatcherResultPrinter(int numTests) { mResultTemplate = new Bundle(); mResultTemplate.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID); mResultTemplate.putInt(REPORT_KEY_NUM_TOTAL, numTests); mStream = new ByteArrayOutputStream(); mWriter = new PrintStream(mStream); mPrinter = new SimpleResultPrinter(mWriter, false); } /** * send a status for the start of a each test, so long tests can be seen * as "running" */ @Override public void startTest(Test test) { String testClass = test.getClass().getName(); String testName = ((TestCase) test).getName(); mTestResult = new Bundle(mResultTemplate); mTestResult.putString(REPORT_KEY_NAME_CLASS, testClass); mTestResult.putString(REPORT_KEY_NAME_TEST, testName); mTestResult.putInt(REPORT_KEY_NUM_CURRENT, ++mTestNum); // pretty printing if (testClass != null && !testClass.equals(mTestClass)) { mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, String.format("\n%s:", testClass)); mTestClass = testClass; } else { mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, ""); } Method testMethod = null; try { testMethod = test.getClass().getMethod(testName); // Report total number of iterations, if test is repetitive if (testMethod.isAnnotationPresent(RepetitiveTest.class)) { int numIterations = testMethod.getAnnotation(RepetitiveTest.class) .numIterations(); mTestResult.putInt(REPORT_KEY_NUM_ITERATIONS, numIterations); } } catch (NoSuchMethodException e) { // ignore- the test with given name does not exist. Will be // handled during test // execution } mAutomationSupport.sendStatus(REPORT_VALUE_RESULT_START, mTestResult); mTestResultCode = 0; mPrinter.startTest(test); } @Override public void addError(Test test, Throwable t) { mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t)); mTestResultCode = REPORT_VALUE_RESULT_ERROR; // pretty printing mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, String.format("\nError in %s:\n%s", ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t))); mPrinter.addError(test, t); } @Override public void addFailure(Test test, AssertionFailedError t) { mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t)); mTestResultCode = REPORT_VALUE_RESULT_FAILURE; // pretty printing mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, String.format("\nFailure in %s:\n%s", ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t))); mPrinter.addFailure(test, t); } @Override public void endTest(Test test) { if (mTestResultCode == 0) { mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "."); } mAutomationSupport.sendStatus(mTestResultCode, mTestResult); mPrinter.endTest(test); } @Override public void print(TestResult result, long runTime, Bundle testOutput) { mPrinter.print(result, runTime, testOutput); testOutput.putString(Instrumentation.REPORT_KEY_STREAMRESULT, String.format("\nTest results for %s=%s", getClass().getSimpleName(), mStream.toString())); mWriter.close(); mAutomationSupport.sendStatus(Activity.RESULT_OK, testOutput); } @Override public void printUnexpectedError(Throwable t) { mWriter.println(String.format("Test run aborted due to unexpected exception: %s", t.getMessage())); t.printStackTrace(mWriter); } } /** * Class that produces the same output as JUnit when running from command line. Can be * used when default UiAutomator output is too verbose. */ private class SimpleResultPrinter extends ResultPrinter implements ResultReporter { private final boolean mFullOutput; public SimpleResultPrinter(PrintStream writer, boolean fullOutput) { super(writer); mFullOutput = fullOutput; } @Override public void print(TestResult result, long runTime, Bundle testOutput) { printHeader(runTime); if (mFullOutput) { printErrors(result); printFailures(result); } printFooter(result); } @Override public void printUnexpectedError(Throwable t) { if (mFullOutput) { getWriter().printf("Test run aborted due to unexpected exeption: %s", t.getMessage()); t.printStackTrace(getWriter()); } } } protected TestCaseCollector getTestCaseCollector(ClassLoader classLoader) { return new TestCaseCollector(classLoader, getTestCaseFilter()); } /** * Returns an object which determines if the class and its methods should be * accepted into the test suite. * @return */ public UiAutomatorTestCaseFilter getTestCaseFilter() { return new UiAutomatorTestCaseFilter(); } protected void addTestListener(TestListener listener) { if (!mTestListeners.contains(listener)) { mTestListeners.add(listener); } } protected void removeTestListener(TestListener listener) { mTestListeners.remove(listener); } /** * subclass may override this method to perform further preparation * * @param testCase */ protected void prepareTestCase(TestCase testCase) { ((UiAutomatorTestCase)testCase).setAutomationSupport(mAutomationSupport); ((UiAutomatorTestCase)testCase).setUiDevice(mUiDevice); ((UiAutomatorTestCase)testCase).setParams(mParams); } }