diff options
Diffstat (limited to 'testing')
15 files changed, 913 insertions, 56 deletions
diff --git a/testing/android/appurify_support.gyp b/testing/android/appurify_support.gyp new file mode 100644 index 0000000..2904368 --- /dev/null +++ b/testing/android/appurify_support.gyp @@ -0,0 +1,22 @@ +# 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. + +{ + 'conditions': [ + ['OS=="android"', { + 'targets': [ + { + 'target_name': 'appurify_support_java', + 'type': 'none', + 'variables': { + 'java_in_dir': '../../testing/android/appurify_support/java', + }, + 'includes': [ + '../../build/java.gypi', + ], + }, + ], + }], + ], +} diff --git a/testing/android/appurify_support/BUILD.gn b/testing/android/appurify_support/BUILD.gn new file mode 100644 index 0000000..871f9d0 --- /dev/null +++ b/testing/android/appurify_support/BUILD.gn @@ -0,0 +1,15 @@ +# 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. + +import("//build/config/android/rules.gni") + +# GYP: //testing/android/appurify_support.gyp:appurify_support_java +android_library("appurify_support_java") { + chromium_code = true + + java_files = [ + "java/src/org/chromium/test/support/ResultsBundleGenerator.java", + "java/src/org/chromium/test/support/RobotiumBundleGenerator.java", + ] +} diff --git a/testing/android/appurify_support/java/src/org/chromium/test/support/ResultsBundleGenerator.java b/testing/android/appurify_support/java/src/org/chromium/test/support/ResultsBundleGenerator.java new file mode 100644 index 0000000..c9588d9 --- /dev/null +++ b/testing/android/appurify_support/java/src/org/chromium/test/support/ResultsBundleGenerator.java @@ -0,0 +1,30 @@ +// 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.support; + +import android.os.Bundle; + +import java.util.Map; + +/** + * Creates a results Bundle. + */ +public interface ResultsBundleGenerator { + + /** Indicates the state of a test. + */ + static enum TestResult { + PASSED, FAILED, ERROR, UNKNOWN + } + + /** Creates a bundle of test results from the provided raw results. + + Note: actual bundle content and format may vary. + + @param rawResults A map between test names and test results. + */ + Bundle generate(Map<String, TestResult> rawResults); +} + diff --git a/testing/android/appurify_support/java/src/org/chromium/test/support/RobotiumBundleGenerator.java b/testing/android/appurify_support/java/src/org/chromium/test/support/RobotiumBundleGenerator.java new file mode 100644 index 0000000..167e7b9 --- /dev/null +++ b/testing/android/appurify_support/java/src/org/chromium/test/support/RobotiumBundleGenerator.java @@ -0,0 +1,56 @@ +// 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.support; + +import android.app.Instrumentation; +import android.os.Bundle; +import android.util.Log; + +import java.util.Map; + +/** + * Creates a results bundle that emulates the one created by Robotium. + */ +public class RobotiumBundleGenerator implements ResultsBundleGenerator { + + private static final String TAG = "RobotiumBundleGenerator"; + + public Bundle generate(Map<String, ResultsBundleGenerator.TestResult> rawResults) { + int testsPassed = 0; + int testsFailed = 0; + + for (Map.Entry<String, ResultsBundleGenerator.TestResult> entry : rawResults.entrySet()) { + switch (entry.getValue()) { + case PASSED: + ++testsPassed; + break; + case FAILED: + // TODO(jbudorick): Remove this log message once AMP execution and + // results handling has been stabilized. + Log.d(TAG, "FAILED: " + entry.getKey()); + ++testsFailed; + break; + default: + Log.w(TAG, "Unhandled: " + entry.getKey() + ", " + + entry.getValue().toString()); + break; + } + } + + StringBuilder resultBuilder = new StringBuilder(); + if (testsFailed > 0) { + resultBuilder.append( + "\nFAILURES!!! Tests run: " + Integer.toString(rawResults.size()) + + ", Failures: " + Integer.toString(testsFailed) + ", Errors: 0"); + } else { + resultBuilder.append("\nOK (" + Integer.toString(testsPassed) + " tests)"); + } + + Bundle resultsBundle = new Bundle(); + resultsBundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + resultBuilder.toString()); + return resultsBundle; + } +} diff --git a/testing/android/broker/BUILD.gn b/testing/android/broker/BUILD.gn new file mode 100644 index 0000000..8daa040 --- /dev/null +++ b/testing/android/broker/BUILD.gn @@ -0,0 +1,13 @@ +# 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. + +import("//build/config/android/rules.gni") + +# GYP: //testing/android/on_device_instrumentation.gyp:broker_java +android_library("broker_java") { + chromium_code = true + + java_files = + [ "java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java" ] +} diff --git a/testing/android/broker/java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java b/testing/android/broker/java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java new file mode 100644 index 0000000..cd755d0 --- /dev/null +++ b/testing/android/broker/java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java @@ -0,0 +1,64 @@ +// 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.broker; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +/** + * An Activity target for OnDeviceInstrumentationDriver that starts the specified + * Instrumentation test. + */ +public class OnDeviceInstrumentationBroker extends Activity { + + public static final String EXTRA_INSTRUMENTATION_PACKAGE = + "org.chromium.test.broker.OnDeviceInstrumentationBroker." + + "InstrumentationPackage"; + public static final String EXTRA_INSTRUMENTATION_CLASS = + "org.chromium.test.broker.OnDeviceInstrumentationBroker." + + "InstrumentationClass"; + public static final String EXTRA_TARGET_ARGS = + "org.chromium.test.broker.OnDeviceInstrumentationBroker.TargetArgs"; + public static final String EXTRA_TEST = + "org.chromium.test.broker.OnDeviceInstrumentationBroker.Test"; + + private static final String TAG = "OnDeviceInstrumentationBroker"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "onCreate()"); + } + + @Override + public void onStart() { + super.onStart(); + + Intent i = getIntent(); + String instrumentationPackage = i.getStringExtra(EXTRA_INSTRUMENTATION_PACKAGE); + String instrumentationClass = i.getStringExtra(EXTRA_INSTRUMENTATION_CLASS); + Bundle targetArgs = i.getBundleExtra(EXTRA_TARGET_ARGS); + String test = i.getStringExtra(EXTRA_TEST); + + if (instrumentationPackage == null || instrumentationClass == null) { + finish(); + return; + } + + ComponentName instrumentationComponent = + new ComponentName(instrumentationPackage, instrumentationClass); + + if (test != null) { + targetArgs.putString("class", test); + } + + startInstrumentation(instrumentationComponent, null, targetArgs); + finish(); + } +} + diff --git a/testing/android/driver/BUILD.gn b/testing/android/driver/BUILD.gn new file mode 100644 index 0000000..436ac63 --- /dev/null +++ b/testing/android/driver/BUILD.gn @@ -0,0 +1,21 @@ +# 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. + +import("//build/config/android/rules.gni") + +# GYP: //testing/android/on_device_instrumentation.gyp:driver_apk +android_apk("driver_apk") { + android_manifest = "java/AndroidManifest.xml" + apk_name = "OnDeviceInstrumentationDriver" + testonly = true + + deps = [ + "//testing/android/appurify_support:appurify_support_java", + "//testing/android/broker:broker_java", + "//testing/android/reporter:reporter_java", + ] + + java_files = + [ "java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java" ] +} diff --git a/testing/android/driver/java/AndroidManifest.xml b/testing/android/driver/java/AndroidManifest.xml new file mode 100644 index 0000000..c7e99ef --- /dev/null +++ b/testing/android/driver/java/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.chromium.test.driver" + android:versionCode="1" + android:versionName="1.0"> + + <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" /> + + <application android:label="OnDeviceInstrumentationDriver" /> + + <instrumentation android:name="org.chromium.test.driver.OnDeviceInstrumentationDriver" + android:targetPackage="org.chromium.test.driver" + android:label="OnDeviceInstrumentationDriver"/> + + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + +</manifest> diff --git a/testing/android/driver/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java b/testing/android/driver/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java new file mode 100644 index 0000000..78c571a --- /dev/null +++ b/testing/android/driver/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java @@ -0,0 +1,271 @@ +// 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.os.Bundle; +import android.os.Environment; +import android.test.InstrumentationTestRunner; +import android.util.Log; + +import org.chromium.test.broker.OnDeviceInstrumentationBroker; +import org.chromium.test.reporter.TestStatusReceiver; +import org.chromium.test.reporter.TestStatusReporter; +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.regex.Pattern; + +/** + * An Instrumentation that drives instrumentation tests from outside the app. + */ +public class OnDeviceInstrumentationDriver extends Instrumentation { + + 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 TEST_WAIT_TIMEOUT = 5 * TestStatusReporter.HEARTBEAT_INTERVAL_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(); + + // 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() { + 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; + + public Driver(String targetPackage, String targetClass, Bundle targetArgs, + List<String> testClasses) { + mTargetPackage = targetPackage; + mTargetClass = targetClass; + mTargetArgs = targetArgs; + mTestClasses = testClasses; + } + + 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); + } + + /** Run the tests. */ + @Override + public void run() { + final HashMap<String, ResultsBundleGenerator.TestResult> finished = + new HashMap<String, ResultsBundleGenerator.TestResult>(); + final Object statusLock = new Object(); + + try { + TestStatusReceiver r = new TestStatusReceiver(); + r.registerCallback(new TestStatusReceiver.StartCallback() { + @Override + public void testStarted(String testClass, String testMethod) { + sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_START, + testClass, testMethod); + synchronized (statusLock) { + statusLock.notify(); + } + } + }); + r.registerCallback(new TestStatusReceiver.PassCallback() { + @Override + public void testPassed(String testClass, String testMethod) { + sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_OK, testClass, + testMethod); + synchronized (statusLock) { + finished.put(testClass + "#" + testMethod, + ResultsBundleGenerator.TestResult.PASSED); + statusLock.notify(); + } + } + }); + r.registerCallback(new TestStatusReceiver.FailCallback() { + @Override + public void testFailed(String testClass, String testMethod) { + sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_ERROR, + testClass, testMethod); + synchronized (statusLock) { + finished.put(testClass + "#" + testMethod, + ResultsBundleGenerator.TestResult.FAILED); + statusLock.notify(); + } + } + }); + r.registerCallback(new TestStatusReceiver.HeartbeatCallback() { + @Override + public void heartbeat() { + Log.i(TAG, "Heartbeat received."); + synchronized (statusLock) { + statusLock.notify(); + } + } + }); + r.register(getContext()); + + for (String t : mTestClasses) { + Intent slaveIntent = new Intent(); + slaveIntent.setComponent(new ComponentName( + mTargetPackage, OnDeviceInstrumentationBroker.class.getName())); + slaveIntent.putExtra( + OnDeviceInstrumentationBroker.EXTRA_INSTRUMENTATION_PACKAGE, + mTargetPackage); + slaveIntent.putExtra( + OnDeviceInstrumentationBroker.EXTRA_INSTRUMENTATION_CLASS, + mTargetClass); + slaveIntent.putExtra(OnDeviceInstrumentationBroker.EXTRA_TEST, t); + slaveIntent.putExtra(OnDeviceInstrumentationBroker.EXTRA_TARGET_ARGS, + mTargetArgs); + slaveIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + getContext().startActivity(slaveIntent); + + synchronized (statusLock) { + while (!finished.containsKey(t)) { + long waitStart = System.currentTimeMillis(); + statusLock.wait(TEST_WAIT_TIMEOUT); + if (System.currentTimeMillis() - waitStart > TEST_WAIT_TIMEOUT) { + Log.e(TAG, t + " has gone missing and is assumed to be dead."); + finished.put(t, ResultsBundleGenerator.TestResult.FAILED); + break; + } + } + } + } + getContext().unregisterReceiver(r); + + } catch (InterruptedException e) { + fail("Interrupted while running tests.", e); + return; + } + pass(new RobotiumBundleGenerator().generate(finished)); + } + + } + + 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); + } +} diff --git a/testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java b/testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java index 4e1a067..25b6570 100644 --- a/testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java +++ b/testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java @@ -12,6 +12,9 @@ import android.os.Bundle; import android.os.Environment; import android.util.Log; +import org.chromium.test.support.ResultsBundleGenerator; +import org.chromium.test.support.RobotiumBundleGenerator; + import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; @@ -38,10 +41,6 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation { private static final int ACCEPT_TIMEOUT_MS = 5000; private static final Pattern RE_TEST_OUTPUT = Pattern.compile("\\[ *([^ ]*) *\\] ?([^ ]+) .*"); - private static interface ResultsBundleGenerator { - public Bundle generate(Map<String, TestResult> rawResults); - } - private String mCommandLineFile; private String mCommandLineFlags; private File mStdoutFile; @@ -91,7 +90,7 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation { } Log.i(TAG, "Getting results."); - Map<String, TestResult> results = parseResults(activityUnderTest); + Map<String, ResultsBundleGenerator.TestResult> results = parseResults(activityUnderTest); Log.i(TAG, "Parsing results and generating output."); return mBundleGenerator.generate(results); @@ -117,16 +116,14 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation { return startActivitySync(i); } - private static enum TestResult { - PASSED, FAILED, ERROR, UNKNOWN - } - /** * Generates a map between test names and test results from the instrumented Activity's * output. */ - private Map<String, TestResult> parseResults(Activity activityUnderTest) { - Map<String, TestResult> results = new HashMap<String, TestResult>(); + private Map<String, ResultsBundleGenerator.TestResult> parseResults( + Activity activityUnderTest) { + Map<String, ResultsBundleGenerator.TestResult> results = + new HashMap<String, ResultsBundleGenerator.TestResult>(); BufferedReader r = null; @@ -145,14 +142,14 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation { boolean isFailure = false; if (m.matches()) { if (m.group(1).equals("RUN")) { - results.put(m.group(2), TestResult.UNKNOWN); + results.put(m.group(2), ResultsBundleGenerator.TestResult.UNKNOWN); } else if (m.group(1).equals("FAILED")) { - results.put(m.group(2), TestResult.FAILED); + results.put(m.group(2), ResultsBundleGenerator.TestResult.FAILED); isFailure = true; mLogBundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT, l + "\n"); sendStatus(0, mLogBundle); } else if (m.group(1).equals("OK")) { - results.put(m.group(2), TestResult.PASSED); + results.put(m.group(2), ResultsBundleGenerator.TestResult.PASSED); } } @@ -185,46 +182,4 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation { return results; } - /** - * Creates a results bundle that emulates the one created by Robotium. - */ - private static class RobotiumBundleGenerator implements ResultsBundleGenerator { - public Bundle generate(Map<String, TestResult> rawResults) { - Bundle resultsBundle = new Bundle(); - - int testsPassed = 0; - int testsFailed = 0; - - for (Map.Entry<String, TestResult> entry : rawResults.entrySet()) { - switch (entry.getValue()) { - case PASSED: - ++testsPassed; - break; - case FAILED: - // TODO(jbudorick): Remove this log message once AMP execution and - // results handling has been stabilized. - Log.d(TAG, "FAILED: " + entry.getKey()); - ++testsFailed; - break; - default: - Log.w(TAG, "Unhandled: " + entry.getKey() + ", " - + entry.getValue().toString()); - break; - } - } - - StringBuilder resultBuilder = new StringBuilder(); - if (testsFailed > 0) { - resultBuilder.append( - "\nFAILURES!!! Tests run: " + Integer.toString(rawResults.size()) - + ", Failures: " + Integer.toString(testsFailed) + ", Errors: 0"); - } else { - resultBuilder.append("\nOK (" + Integer.toString(testsPassed) + " tests)"); - } - resultsBundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT, - resultBuilder.toString()); - return resultsBundle; - } - } - } diff --git a/testing/android/on_device_instrumentation.gyp b/testing/android/on_device_instrumentation.gyp new file mode 100644 index 0000000..12e0f35 --- /dev/null +++ b/testing/android/on_device_instrumentation.gyp @@ -0,0 +1,79 @@ +# 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. + +{ + 'conditions': [ + ['OS=="android"', { + 'variables' : { + 'driver_apk_name': 'OnDeviceInstrumentationDriver', + 'driver_apk_path': '<(PRODUCT_DIR)/apks/<(driver_apk_name).apk' + }, + 'targets': [ + { + 'target_name': 'reporter_java', + 'type': 'none', + 'dependencies': ['../../base/base.gyp:base_java'], + 'variables': { + 'java_in_dir': '../../testing/android/reporter/java', + }, + 'includes': [ + '../../build/java.gypi', + ], + }, + { + 'target_name': 'broker_java', + 'type': 'none', + 'variables': { + 'java_in_dir': '../../testing/android/broker/java', + }, + 'includes': [ + '../../build/java.gypi', + ], + }, + { + 'target_name': 'driver_apk', + 'type': 'none', + 'dependencies': [ + 'broker_java', + 'reporter_java', + 'appurify_support.gyp:appurify_support_java', + ], + 'variables': { + 'apk_name': '<(driver_apk_name)', + 'final_apk_path': '<(driver_apk_path)', + 'java_in_dir': '../../testing/android/driver/java', + }, + 'includes': [ + '../../build/java_apk.gypi', + ], + }, + { + # This emulates gn's datadeps fields, allowing other APKs to declare + # that they require that this APK be built without including the + # driver's code. + 'target_name': 'require_driver_apk', + 'type': 'none', + 'actions': [ + { + 'action_name': 'require_<(driver_apk_name)', + 'message': 'Making sure <(driver_apk_path) has been built.', + 'variables': { + 'required_file': '<(PRODUCT_DIR)/driver_apk/<(driver_apk_name).apk.required', + }, + 'inputs': [ + '<(driver_apk_path)', + ], + 'outputs': [ + '<(required_file)', + ], + 'action': [ + 'python', '../../build/android/gyp/touch.py', '<(required_file)', + ], + }, + ], + }, + ], + }], + ], +} diff --git a/testing/android/reporter/BUILD.gn b/testing/android/reporter/BUILD.gn new file mode 100644 index 0000000..7086a79 --- /dev/null +++ b/testing/android/reporter/BUILD.gn @@ -0,0 +1,19 @@ +# 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. + +import("//build/config/android/rules.gni") + +# GYP: //testing/android/on_device_instrumentation.gyp:reporter_java +android_library("reporter_java") { + chromium_code = true + + deps = [ + "//base:base_java", + ] + java_files = [ + "java/src/org/chromium/test/reporter/TestStatusListener.java", + "java/src/org/chromium/test/reporter/TestStatusReceiver.java", + "java/src/org/chromium/test/reporter/TestStatusReporter.java", + ] +} diff --git a/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusListener.java b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusListener.java new file mode 100644 index 0000000..6275418 --- /dev/null +++ b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusListener.java @@ -0,0 +1,78 @@ +// 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.reporter; + +import android.content.Context; +import android.util.Log; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestListener; + +/** + * A TestListener that reports when tests start, pass, or fail. + */ +public class TestStatusListener implements TestListener { + + private static final String TAG = "TestStatusListener"; + + private boolean mFailed; + private final TestStatusReporter mReporter; + + public TestStatusListener(Context context) { + mReporter = new TestStatusReporter(context); + } + + /** Called when an error has occurred while running a test. + + Note that an error usually means a problem with the test or test harness, not with + the code under test. + + @param test The test in which the error occurred. + @param t The exception that was raised. + */ + @Override + public void addError(Test test, Throwable t) { + Log.e(TAG, "Error while running " + test.toString(), t); + mFailed = true; + } + + /** Called when a test has failed. + + @param test The test in which the failure occurred. + @param t The exception that was raised. + */ + public void addFailure(Test test, AssertionFailedError e) { + Log.e(TAG, "Failure while running " + test.toString(), e); + mFailed = true; + } + + /** Called when a test has started. + @param test The test that started. + */ + @Override + public void startTest(Test test) { + mFailed = false; + TestCase testCase = (TestCase) test; + mReporter.startHeartbeat(); + mReporter.testStarted(testCase.getClass().getName(), testCase.getName()); + } + + /** Called when a test has ended. + @param test The test that ended. + */ + @Override + public void endTest(Test test) { + TestCase testCase = (TestCase) test; + if (mFailed) { + mReporter.testFailed(testCase.getClass().getName(), testCase.getName()); + } else { + mReporter.testPassed(testCase.getClass().getName(), testCase.getName()); + } + mReporter.stopHeartbeat(); + } + +} diff --git a/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReceiver.java b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReceiver.java new file mode 100644 index 0000000..e4af9b6 --- /dev/null +++ b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReceiver.java @@ -0,0 +1,128 @@ +// 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.reporter; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** Receives test status broadcasts send from + {@link org.chromium.test.reporter.TestStatusReporter}. + */ +public class TestStatusReceiver extends BroadcastReceiver { + + private static final String TAG = "ResultReceiver"; + + private final List<FailCallback> mFailCallbacks = new ArrayList<FailCallback>(); + private final List<HeartbeatCallback> mHeartbeatCallbacks = new ArrayList<HeartbeatCallback>(); + private final List<PassCallback> mPassCallbacks = new ArrayList<PassCallback>(); + private final List<StartCallback> mStartCallbacks = new ArrayList<StartCallback>(); + + /** An IntentFilter that matches the intents that this class can receive. */ + private static final IntentFilter INTENT_FILTER; + static { + IntentFilter filter = new IntentFilter(); + filter.addAction(TestStatusReporter.ACTION_HEARTBEAT); + filter.addAction(TestStatusReporter.ACTION_TEST_FAILED); + filter.addAction(TestStatusReporter.ACTION_TEST_PASSED); + filter.addAction(TestStatusReporter.ACTION_TEST_STARTED); + try { + filter.addDataType(TestStatusReporter.DATA_TYPE_HEARTBEAT); + filter.addDataType(TestStatusReporter.DATA_TYPE_RESULT); + } catch (IntentFilter.MalformedMimeTypeException e) { + Log.wtf(TAG, "Invalid MIME type", e); + } + INTENT_FILTER = filter; + } + + /** A callback used when a test has failed. */ + public interface FailCallback { + void testFailed(String testClass, String testMethod); + } + + /** A callback used when a heartbeat is received. */ + public interface HeartbeatCallback { + void heartbeat(); + } + + /** A callback used when a test has passed. */ + public interface PassCallback { + void testPassed(String testClass, String testMethod); + } + + /** A callback used when a test has started. */ + public interface StartCallback { + void testStarted(String testClass, String testMethod); + } + + /** Register a callback for when a test has failed. */ + public void registerCallback(FailCallback c) { + mFailCallbacks.add(c); + } + + /** Register a callback for when a heartbeat is received. */ + public void registerCallback(HeartbeatCallback c) { + mHeartbeatCallbacks.add(c); + } + + /** Register a callback for when a test has passed. */ + public void registerCallback(PassCallback c) { + mPassCallbacks.add(c); + } + + /** Register a callback for when a test has started. */ + public void registerCallback(StartCallback c) { + mStartCallbacks.add(c); + } + + /** Register this receiver using the provided context. */ + public void register(Context c) { + c.registerReceiver(this, INTENT_FILTER); + } + + /** Receive a broadcast intent. + * + * @param context The Context in which the receiver is running. + * @param intent The intent received. + */ + @Override + public void onReceive(Context context, Intent intent) { + String testClass = intent.getStringExtra(TestStatusReporter.EXTRA_TEST_CLASS); + String testMethod = intent.getStringExtra(TestStatusReporter.EXTRA_TEST_METHOD); + + switch (intent.getAction()) { + case TestStatusReporter.ACTION_TEST_STARTED: + for (StartCallback c : mStartCallbacks) { + c.testStarted(testClass, testMethod); + } + break; + case TestStatusReporter.ACTION_TEST_PASSED: + for (PassCallback c : mPassCallbacks) { + c.testPassed(testClass, testMethod); + } + break; + case TestStatusReporter.ACTION_TEST_FAILED: + for (FailCallback c : mFailCallbacks) { + c.testFailed(testClass, testMethod); + } + break; + case TestStatusReporter.ACTION_HEARTBEAT: + for (HeartbeatCallback c : mHeartbeatCallbacks) { + c.heartbeat(); + } + break; + default: + Log.e(TAG, "Unrecognized intent received: " + intent.toString()); + break; + } + } + +} + diff --git a/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReporter.java b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReporter.java new file mode 100644 index 0000000..6ac7312 --- /dev/null +++ b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReporter.java @@ -0,0 +1,83 @@ +// 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.reporter; + +import android.content.Context; +import android.content.Intent; + +import org.chromium.base.ThreadUtils; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Broadcasts test status to any listening {@link org.chromium.test.reporter.TestStatusReceiver}. + */ +public class TestStatusReporter { + + public static final String ACTION_HEARTBEAT = + "org.chromium.test.reporter.TestStatusReporter.HEARTBEAT"; + public static final String ACTION_TEST_STARTED = + "org.chromium.test.reporter.TestStatusReporter.TEST_STARTED"; + public static final String ACTION_TEST_PASSED = + "org.chromium.test.reporter.TestStatusReporter.TEST_PASSED"; + public static final String ACTION_TEST_FAILED = + "org.chromium.test.reporter.TestStatusReporter.TEST_FAILED"; + public static final String DATA_TYPE_HEARTBEAT = "org.chromium.test.reporter/heartbeat"; + public static final String DATA_TYPE_RESULT = "org.chromium.test.reporter/result"; + public static final String EXTRA_TEST_CLASS = + "org.chromium.test.reporter.TestStatusReporter.TEST_CLASS"; + public static final String EXTRA_TEST_METHOD = + "org.chromium.test.reporter.TestStatusReporter.TEST_METHOD"; + + public static final int HEARTBEAT_INTERVAL_MS = 5000; + + private final Context mContext; + private final AtomicBoolean mKeepBeating = new AtomicBoolean(false); + + public TestStatusReporter(Context c) { + mContext = c; + } + + public void startHeartbeat() { + mKeepBeating.set(true); + Runnable heartbeat = new Runnable() { + @Override + public void run() { + Intent i = new Intent(ACTION_HEARTBEAT); + i.setType(DATA_TYPE_HEARTBEAT); + mContext.sendBroadcast(i); + if (mKeepBeating.get()) { + ThreadUtils.postOnUiThreadDelayed(this, HEARTBEAT_INTERVAL_MS); + } + } + }; + ThreadUtils.postOnUiThreadDelayed(heartbeat, HEARTBEAT_INTERVAL_MS); + } + + public void testStarted(String testClass, String testMethod) { + sendBroadcast(testClass, testMethod, ACTION_TEST_STARTED); + } + + public void testPassed(String testClass, String testMethod) { + sendBroadcast(testClass, testMethod, ACTION_TEST_PASSED); + } + + public void testFailed(String testClass, String testMethod) { + sendBroadcast(testClass, testMethod, ACTION_TEST_FAILED); + } + + public void stopHeartbeat() { + mKeepBeating.set(false); + } + + private void sendBroadcast(String testClass, String testMethod, String action) { + Intent i = new Intent(action); + i.setType(DATA_TYPE_RESULT); + i.putExtra(EXTRA_TEST_CLASS, testClass); + i.putExtra(EXTRA_TEST_METHOD, testMethod); + mContext.sendBroadcast(i); + } + +} |