Chromium Code Reviews| Index: testing/android/java/src/org/chromium/test/outstrumentation/master/Outstrumentation.java |
| diff --git a/testing/android/java/src/org/chromium/test/outstrumentation/master/Outstrumentation.java b/testing/android/java/src/org/chromium/test/outstrumentation/master/Outstrumentation.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..db3c1b1e639e36c168390057174b2a96acd381ab |
| --- /dev/null |
| +++ b/testing/android/java/src/org/chromium/test/outstrumentation/master/Outstrumentation.java |
| @@ -0,0 +1,356 @@ |
| +// 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.outstrumentation.master; |
| + |
| +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.util.Log; |
| + |
| +import org.chromium.test.outstrumentation.reporter.ReportingService; |
| +import org.chromium.test.outstrumentation.slave.OutstrumentationSlaveActivity; |
| +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.) |
| + */ |
| +public class Outstrumentation extends Instrumentation { |
| + |
| + private static final String TAG = "Outstrumentation"; |
| + |
| + private static final String EXTRA_TEST_LIST = |
| + "org.chromium.test.outstrumentation.master.Outstrumentation.TestList"; |
| + private static final String EXTRA_TEST_LIST_FILE = |
| + "org.chromium.test.outstrumentation.master.Outstrumentation.TestListFile"; |
| + private static final String EXTRA_TARGET_PACKAGE = |
| + "org.chromium.test.outstrumentation.master.Outstrumentation.TargetPackage"; |
| + private static final String EXTRA_TARGET_CLASS = |
| + "org.chromium.test.outstrumentation.master.Outstrumentation.TargetClass"; |
| + |
| + private static final Pattern COMMA = Pattern.compile(","); |
| + |
| + private Bundle mTargetArgs; |
| + private String mTargetClass; |
| + private String mTargetPackage; |
| + private List<String> mTestClasses; |
| + |
| + @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); |
|
Ted C
2015/04/06 17:43:54
why remove things?
jbudorick
2015/04/07 00:54:38
I want Outstrumentation (% name change) to forward
|
| + |
| + 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); |
|
Ted C
2015/04/06 17:43:54
java indenting is 8 from the previous line
jbudorick
2015/04/07 00:54:39
Done.
|
| + try { |
| + BufferedReader testListFileReader = |
| + new BufferedReader(new FileReader(testListFile)); |
| + while (true) { |
|
Ted C
2015/04/06 17:43:54
nit: I normally see this as:
String test;
while (
jbudorick
2015/04/07 00:54:39
Done.
|
| + String test = testListFileReader.readLine(); |
| + if (test == null) break; |
| + 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; |
| + } |
| + |
| + start(); |
| + } |
| + |
| + @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. |
| + Thread driverThread = new Thread( |
| + new Driver(mTargetPackage, mTargetClass, mTargetArgs, mTestClasses)); |
| + driverThread.start(); |
| + } |
| + |
| + @Override |
| + public void onDestroy() { |
|
Ted C
2015/04/06 17:43:54
onStop is the corollary to onStart. So the thread
jbudorick
2015/04/07 00:54:38
Moved thread creation to onCreate and added a bool
|
| + getContext().stopService(new Intent(getContext(), ReportingService.class)); |
| + super.onDestroy(); |
| + } |
| + |
| + private class Driver implements Runnable { |
| + |
| + private static final String TAG = Outstrumentation.TAG + ".Driver"; |
| + |
| + private Messenger mReportingService; |
|
Ted C
2015/04/06 17:43:54
why not declared inside of ReportingServiceConnect
jbudorick
2015/04/07 00:54:39
I'm not actually sure why I did that; I imagine th
|
| + private Object mServiceLock = new Object(); |
| + 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"; |
| + |
| + @Override |
| + public void onServiceConnected(ComponentName name, IBinder service) { |
| + // Register receiver. |
| + Log.i(TAG, "service connected"); |
| + synchronized (mServiceLock) { |
| + mReportingService = new Messenger(service); |
| + mServiceLock.notify(); |
| + } |
| + } |
| + |
| + public void registerWithReportingService(Messenger receiver) |
|
Ted C
2015/04/06 17:43:54
javadoc all the public methods (and in ReportHandl
jbudorick
2015/04/07 00:54:39
Done.
|
| + throws InterruptedException, RemoteException { |
| + synchronized (mServiceLock) { |
| + Log.i(TAG, "acquired service lock"); |
| + while (mReportingService == null) { |
| + Log.i(TAG, "waiting for service connection."); |
| + mServiceLock.wait(); |
| + } |
| + |
| + 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"); |
| + } |
| + } |
| + |
| + public void unregisterWithReportingService() |
| + throws InterruptedException, RemoteException { |
| + synchronized (mServiceLock) { |
| + if (mReportingService == null) return; |
| + |
| + Message registration = Message.obtain(); |
| + registration.what = ReportingService.MSG_UNREGISTER_RECEIVER; |
| + mReportingService.send(registration); |
| + } |
| + } |
| + |
| + @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); |
| + } |
| + |
| + @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: |
|
Ted C
2015/04/06 17:43:54
probably should fail in this case as it would indi
jbudorick
2015/04/07 00:54:39
Done.
|
| + break; |
| + } |
| + } |
| + } |
| + |
| + public void waitForTestFinished(String testName) throws InterruptedException { |
| + synchronized (mLock) { |
| + while (!mFinished.containsKey(testName)) { |
| + mLock.wait(); |
| + } |
| + } |
| + } |
| + |
| + public Bundle getResults() { |
| + return new RobotiumBundleGenerator().generate(mFinished); |
| + } |
| + } |
| + |
| + @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; |
| + } |
| + |
| + c.registerWithReportingService(new Messenger(mHandler)); |
| + |
| + for (String t : mTestClasses) { |
| + Intent slaveIntent = new Intent(); |
| + slaveIntent.setComponent( |
| + new ComponentName(mTargetPackage, |
| + OutstrumentationSlaveActivity.class.getName())); |
|
Ted C
2015/04/06 17:43:54
same indent comment...8 from the previous line
jbudorick
2015/04/07 00:54:39
the troubles with jumping between languages
done.
|
| + slaveIntent.putExtra( |
| + OutstrumentationSlaveActivity.EXTRA_INSTRUMENTATION_PACKAGE, |
| + mTargetPackage); |
| + slaveIntent.putExtra( |
| + OutstrumentationSlaveActivity.EXTRA_INSTRUMENTATION_CLASS, |
| + mTargetClass); |
| + slaveIntent.putExtra(OutstrumentationSlaveActivity.EXTRA_TEST, t); |
| + slaveIntent.putExtra(OutstrumentationSlaveActivity.EXTRA_TARGET_ARGS, |
| + mTargetArgs); |
| + slaveIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| + |
| + getContext().startActivity(slaveIntent); |
| + |
| + mHandler.waitForTestFinished(t); |
| + } |
| + |
| + c.unregisterWithReportingService(); |
| + 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) { |
| + Bundle status = new Bundle(); |
| + status.putString("class", testClass); |
| + status.putString("test", testMethod); |
| + sendStatus(1, status); |
|
Ted C
2015/04/06 17:43:54
where are these status values defined (1, 0, -1)?
jbudorick
2015/04/07 00:54:39
I had thought they were private constants in Instr
|
| + } |
| + |
| + private void testPassed(String testClass, String testMethod) { |
| + Bundle status = new Bundle(); |
| + status.putString("class", testClass); |
| + status.putString("test", testMethod); |
| + sendStatus(0, status); |
| + } |
| + |
| + private void testFailed(String testClass, String testMethod) { |
| + Bundle status = new Bundle(); |
| + status.putString("class", testClass); |
| + status.putString("test", testMethod); |
| + sendStatus(-1, status); |
| + } |
| +} |