Index: testing/android/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java |
diff --git a/testing/android/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java b/testing/android/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1daa6fd4198cef68118bbe868923ec5b7dd96136 |
--- /dev/null |
+++ b/testing/android/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java |
@@ -0,0 +1,412 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.test.driver; |
+ |
+import android.app.Activity; |
+import android.app.Instrumentation; |
+import android.content.ComponentName; |
+import android.content.Intent; |
+import android.content.ServiceConnection; |
+import android.os.Bundle; |
+import android.os.Environment; |
+import android.os.Handler; |
+import android.os.IBinder; |
+import android.os.Looper; |
+import android.os.Message; |
+import android.os.Messenger; |
+import android.os.RemoteException; |
+import android.test.InstrumentationTestRunner; |
+import android.util.Log; |
+ |
+import org.chromium.test.passenger.OnDeviceInstrumentationPassenger; |
+import org.chromium.test.reporter.ReportingService; |
+import org.chromium.test.support.ResultsBundleGenerator; |
+import org.chromium.test.support.RobotiumBundleGenerator; |
+ |
+import java.io.BufferedReader; |
+import java.io.File; |
+import java.io.FileReader; |
+import java.io.IOException; |
+import java.util.ArrayList; |
+import java.util.Arrays; |
+import java.util.HashMap; |
+import java.util.List; |
+import java.util.Map; |
+import java.util.regex.Pattern; |
+ |
+/** |
+ * An Instrumentation that drives instrumentation tests from outside the app. (Hence the name.) |
Yaron
2015/04/07 15:39:04
I think you can now drop "(Hence the name.)"
jbudorick
2015/04/07 19:24:45
oops, done.
|
+ */ |
+public class OnDeviceInstrumentationDriver extends Instrumentation { |
jbudorick
2015/04/07 15:09:22
Renamed:
Outstrumentation -> OnDeviceInstrumenta
Yaron
2015/04/07 15:39:04
I think it would be helpful to have a mini-dd or o
jbudorick
2015/04/07 19:24:45
Can do. I've got it on a whiteboard right now and
|
+ |
+ private static final String TAG = "OnDeviceInstrumentationDriver"; |
+ |
+ private static final String EXTRA_TEST_LIST = |
+ "org.chromium.test.driver.OnDeviceInstrumentationDriver.TestList"; |
+ private static final String EXTRA_TEST_LIST_FILE = |
+ "org.chromium.test.driver.OnDeviceInstrumentationDriver.TestListFile"; |
+ private static final String EXTRA_TARGET_PACKAGE = |
+ "org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetPackage"; |
+ private static final String EXTRA_TARGET_CLASS = |
+ "org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetClass"; |
+ |
+ private static final Pattern COMMA = Pattern.compile(","); |
+ private static final int SERVICE_WAIT_TIMEOUT = 5000; // ms |
+ private static final int TEST_WAIT_TIMEOUT = 10000; // ms |
+ |
+ private boolean mDriverStarted; |
+ private Thread mDriverThread; |
+ private Bundle mTargetArgs; |
+ private String mTargetClass; |
+ private String mTargetPackage; |
+ private List<String> mTestClasses; |
+ |
+ /** Parse any arguments and prepare to run tests. |
+ |
+ @param arguments The arguments to parse. |
+ */ |
+ @Override |
+ public void onCreate(Bundle arguments) { |
+ mTargetArgs = new Bundle(arguments); |
+ mTargetPackage = arguments.getString(EXTRA_TARGET_PACKAGE); |
+ if (mTargetPackage == null) { |
+ fail("No target package."); |
+ return; |
+ } |
+ mTargetArgs.remove(EXTRA_TARGET_PACKAGE); |
+ |
+ mTargetClass = arguments.getString(EXTRA_TARGET_CLASS); |
+ if (mTargetClass == null) { |
+ fail("No target class."); |
+ return; |
+ } |
+ mTargetArgs.remove(EXTRA_TARGET_CLASS); |
+ |
+ mTestClasses = new ArrayList<String>(); |
+ String testList = arguments.getString(EXTRA_TEST_LIST); |
+ if (testList != null) { |
+ mTestClasses.addAll(Arrays.asList(COMMA.split(testList))); |
+ mTargetArgs.remove(EXTRA_TEST_LIST); |
+ } |
+ |
+ String testListFilePath = arguments.getString(EXTRA_TEST_LIST_FILE); |
+ if (testListFilePath != null) { |
+ File testListFile = new File(Environment.getExternalStorageDirectory(), |
+ testListFilePath); |
+ try { |
+ BufferedReader testListFileReader = |
+ new BufferedReader(new FileReader(testListFile)); |
+ String test; |
+ while ((test = testListFileReader.readLine()) != null) { |
+ mTestClasses.add(test); |
+ } |
+ testListFileReader.close(); |
+ } catch (IOException e) { |
+ Log.e(TAG, "Error reading " + testListFile.getAbsolutePath(), e); |
+ } |
+ mTargetArgs.remove(EXTRA_TEST_LIST_FILE); |
+ } |
+ |
+ if (mTestClasses.isEmpty()) { |
+ fail("No tests."); |
+ return; |
+ } |
+ |
+ mDriverThread = new Thread( |
+ new Driver(mTargetPackage, mTargetClass, mTargetArgs, mTestClasses)); |
+ |
+ start(); |
+ } |
+ |
+ /** Start running tests. */ |
+ @Override |
+ public void onStart() { |
+ super.onStart(); |
+ |
+ getContext().startService(new Intent(getContext(), ReportingService.class)); |
+ |
+ // Start the driver on its own thread s.t. it can block while the main thread's |
+ // Looper receives and handles messages. |
+ if (!mDriverStarted) { |
+ mDriverThread.start(); |
+ mDriverStarted = true; |
+ } |
+ } |
+ |
+ /** Clean up the reporting service. */ |
+ @Override |
+ public void onDestroy() { |
+ getContext().stopService(new Intent(getContext(), ReportingService.class)); |
+ super.onDestroy(); |
+ } |
+ |
+ private class Driver implements Runnable { |
+ |
+ private static final String TAG = OnDeviceInstrumentationDriver.TAG + ".Driver"; |
+ |
+ private Bundle mTargetArgs; |
+ private String mTargetClass; |
+ private String mTargetPackage; |
+ private List<String> mTestClasses; |
+ private ReportHandler mHandler; |
+ |
+ public Driver(String targetPackage, String targetClass, Bundle targetArgs, |
+ List<String> testClasses) { |
+ mTargetPackage = targetPackage; |
+ mTargetClass = targetClass; |
+ mTargetArgs = targetArgs; |
+ mTestClasses = testClasses; |
+ mHandler = new ReportHandler(Looper.getMainLooper()); |
+ } |
+ |
+ private class ReportingServiceConnection implements ServiceConnection { |
+ |
+ private static final String TAG = Driver.TAG + ".ReportingServiceConnection"; |
+ |
+ private Messenger mReportingService; |
+ private Object mServiceLock = new Object(); |
+ |
+ /** Called when a service has been connected. |
+ |
+ @param name The name of the service that has been connected. |
+ @param service The IBinder for the service. |
+ */ |
+ @Override |
+ public void onServiceConnected(ComponentName name, IBinder service) { |
+ synchronized (mServiceLock) { |
+ mReportingService = new Messenger(service); |
+ mServiceLock.notify(); |
+ } |
+ } |
+ |
+ /** Registers a {@link android.os.Messenger} with the |
+ {@link org.chromium.test.driver.reporter.ReportingService}. |
+ |
+ This lets the reporting service know that it should relay messages it receives |
+ to the provided receiver. |
+ |
+ @param receiver The object to register with the reporting service. |
+ */ |
+ public void registerWithReportingService(Messenger receiver) |
+ throws InterruptedException, RemoteException { |
+ synchronized (mServiceLock) { |
+ Log.i(TAG, "acquired service lock"); |
+ while (mReportingService == null) { |
+ Log.i(TAG, "waiting for service connection."); |
+ mServiceLock.wait(SERVICE_WAIT_TIMEOUT); |
+ } |
+ |
+ Log.i(TAG, "reporting service non-null"); |
+ |
+ Message registration = Message.obtain(); |
+ registration.what = ReportingService.MSG_REGISTER_RECEIVER; |
+ registration.replyTo = receiver; |
+ mReportingService.send(registration); |
+ |
+ Log.i(TAG, "sent registration message"); |
+ } |
+ } |
+ |
+ /** Unregisters a {@link android.os.Messenger} with the |
+ {@link org.chromium.test.driver.reporter.ReportingService}. |
+ |
+ This lets the reporting service know that it should no longer relay messages it |
+ receives to this object. |
+ |
+ @param receiver The object to unregister from the reporting service. |
+ */ |
+ public void unregisterWithReportingService(Messenger receiver) |
+ throws InterruptedException, RemoteException { |
+ synchronized (mServiceLock) { |
+ if (mReportingService == null) return; |
+ |
+ Message registration = Message.obtain(); |
+ registration.what = ReportingService.MSG_UNREGISTER_RECEIVER; |
+ registration.replyTo = receiver; |
+ mReportingService.send(registration); |
+ } |
+ } |
+ |
+ /** Called when a service has disconnected. |
+ |
+ @param name The name of the service that has disconnected. |
+ */ |
+ @Override |
+ public void onServiceDisconnected(ComponentName name) { |
+ synchronized (mServiceLock) { |
+ mReportingService = null; |
+ mServiceLock.notify(); |
+ } |
+ } |
+ } |
+ |
+ private class ReportHandler extends Handler { |
+ private static final String TAG = Driver.TAG + ".ReportHandler"; |
+ |
+ private final Object mLock = new Object(); |
+ private final Map<String, ResultsBundleGenerator.TestResult> mFinished = |
+ new HashMap<String, ResultsBundleGenerator.TestResult>(); |
+ |
+ public ReportHandler(Looper looper) { |
+ super(looper); |
+ } |
+ |
+ /** Receive a message. |
+ |
+ This does nothing unless the message is a test report from the |
+ {@link org.chromium.test.driver.reporter.ReportingService}. |
+ |
+ @param msg The message to handle. |
+ */ |
+ @Override |
+ public void handleMessage(Message msg) { |
+ switch (msg.what) { |
+ case ReportingService.MSG_REPORT_TEST_STARTED: |
+ case ReportingService.MSG_REPORT_TEST_PASSED: |
+ case ReportingService.MSG_REPORT_TEST_FAILED: |
+ handleTestStatusMessage(msg); |
+ break; |
+ default: |
+ super.handleMessage(msg); |
+ break; |
+ } |
+ } |
+ |
+ private void handleTestStatusMessage(Message msg) { |
+ String testClass = msg.getData().getString(ReportingService.MSG_DATA_TEST_CLASS); |
+ String testMethod = msg.getData().getString(ReportingService.MSG_DATA_TEST_METHOD); |
+ String testName = testClass + "#" + testMethod; |
+ synchronized (mLock) { |
+ switch (msg.what) { |
+ case ReportingService.MSG_REPORT_TEST_STARTED: |
+ testStarted(testClass, testMethod); |
+ break; |
+ case ReportingService.MSG_REPORT_TEST_PASSED: |
+ mFinished.put(testName, ResultsBundleGenerator.TestResult.PASSED); |
+ testPassed(testClass, testMethod); |
+ mLock.notify(); |
+ break; |
+ case ReportingService.MSG_REPORT_TEST_FAILED: |
+ mFinished.put(testName, ResultsBundleGenerator.TestResult.FAILED); |
+ testFailed(testClass, testMethod); |
+ mLock.notify(); |
+ break; |
+ default: |
+ throw new IllegalArgumentException( |
+ "Received unexpected message from reporting service: " |
+ + msg.what); |
+ } |
+ } |
+ } |
+ |
+ /** Wait until the test with the given name has been reported as finished. |
+ |
+ @param testName The name of the test to wait for. |
+ */ |
+ public void waitForTestFinished(String testName) throws InterruptedException { |
+ synchronized (mLock) { |
+ while (!mFinished.containsKey(testName)) { |
+ Log.i(TAG, "Waiting for " + testName + " to finish."); |
+ mLock.wait(TEST_WAIT_TIMEOUT); |
+ } |
+ } |
+ } |
+ |
+ /** Get the test results. |
+ |
+ @return A {@link android.os.Bundle} containing the results of all reported tests. |
+ */ |
+ public Bundle getResults() { |
+ return new RobotiumBundleGenerator().generate(mFinished); |
+ } |
+ } |
+ |
+ /** Run the tests. */ |
+ @Override |
+ public void run() { |
+ try { |
+ ReportingServiceConnection c = new ReportingServiceConnection(); |
+ if (!getContext().bindService( |
+ new Intent(getContext(), ReportingService.class), c, 0)) { |
+ fail("Reporting service not bound."); |
+ return; |
+ } |
+ |
+ Messenger receiver = new Messenger(mHandler); |
+ c.registerWithReportingService(receiver); |
+ |
+ for (String t : mTestClasses) { |
+ Intent slaveIntent = new Intent(); |
+ slaveIntent.setComponent(new ComponentName( |
+ mTargetPackage, OnDeviceInstrumentationPassenger.class.getName())); |
+ slaveIntent.putExtra( |
+ OnDeviceInstrumentationPassenger.EXTRA_INSTRUMENTATION_PACKAGE, |
+ mTargetPackage); |
+ slaveIntent.putExtra( |
+ OnDeviceInstrumentationPassenger.EXTRA_INSTRUMENTATION_CLASS, |
+ mTargetClass); |
+ slaveIntent.putExtra(OnDeviceInstrumentationPassenger.EXTRA_TEST, t); |
+ slaveIntent.putExtra(OnDeviceInstrumentationPassenger.EXTRA_TARGET_ARGS, |
+ mTargetArgs); |
+ slaveIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
+ |
+ getContext().startActivity(slaveIntent); |
+ |
+ mHandler.waitForTestFinished(t); |
+ } |
+ |
+ c.unregisterWithReportingService(receiver); |
+ getContext().unbindService(c); |
+ } catch (RemoteException e) { |
+ fail("Error connecting to reporting service.", e); |
+ return; |
+ } catch (InterruptedException e) { |
+ fail("Interrupted while running tests.", e); |
+ return; |
+ } |
+ pass(mHandler.getResults()); |
+ } |
+ |
+ } |
+ |
+ private void fail(String reason) { |
+ Log.e(TAG, reason); |
+ failImpl(reason); |
+ } |
+ |
+ private void fail(String reason, Exception e) { |
+ Log.e(TAG, reason, e); |
+ failImpl(reason); |
+ } |
+ |
+ private void failImpl(String reason) { |
+ Bundle b = new Bundle(); |
+ b.putString("reason", reason); |
+ finish(Activity.RESULT_CANCELED, b); |
+ } |
+ |
+ private void pass(Bundle results) { |
+ finish(Activity.RESULT_OK, results); |
+ } |
+ |
+ private void testStarted(String testClass, String testMethod) { |
+ sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_START, testClass, testMethod); |
+ } |
+ |
+ private void testPassed(String testClass, String testMethod) { |
+ sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_OK, testClass, testMethod); |
+ } |
+ |
+ private void testFailed(String testClass, String testMethod) { |
+ sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_ERROR, testClass, testMethod); |
+ } |
+ |
+ private void sendTestStatus(int status, String testClass, String testMethod) { |
+ Bundle statusBundle = new Bundle(); |
+ statusBundle.putString(InstrumentationTestRunner.REPORT_KEY_NAME_CLASS, testClass); |
+ statusBundle.putString(InstrumentationTestRunner.REPORT_KEY_NAME_TEST, testMethod); |
+ sendStatus(status, statusBundle); |
+ } |
+} |