Bug 1169476 -- Implement |mach robocop --serve|. r=gbrown

This adds a flag to |mach robocop| that does everything to run a
Robocop test except launch the actual test.  Instead of launching the
test, it starts the mochi.test server and launches Fennec with a test
profile; then it sits and waits forever.

This allows regular Java IDEs (IntelliJ, but previously Eclipse) to
run Robocop tests like regular instrumentation tests, "injecting" them
into the prepared testing environment.  It's quite nice!

--HG--
extra : rebase_source : a5ab08222110a20291aebe70ef1fda0d340dbe7d
extra : source : e91ac9a35f86928fcd519911476ee7d68d06f921
This commit is contained in:
Nick Alexander 2015-05-29 17:18:07 -07:00
parent 30b2eea9fd
commit 97786c5695
9 changed files with 180 additions and 98 deletions

View File

@ -24,6 +24,10 @@ fennecLogcatFilters = [ "The character encoding of the HTML document was not dec
class RemoteAutomation(Automation):
_devicemanager = None
# Part of a hack for Robocop: "am COMMAND" is handled specially if COMMAND
# is in this set. See usages below.
_specialAmCommands = ('instrument', 'start')
def __init__(self, deviceManager, appName = '', remoteLog = None,
processArgs=None):
self._devicemanager = deviceManager
@ -237,7 +241,7 @@ class RemoteAutomation(Automation):
# Hack for robocop, if app & testURL == None and extraArgs contains the rest of the stuff, lets
# assume extraArgs is all we need
if app == "am" and extraArgs[0] == "instrument":
if app == "am" and extraArgs[0] in RemoteAutomation._specialAmCommands:
return app, extraArgs
cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
@ -275,7 +279,7 @@ class RemoteAutomation(Automation):
else:
raise Exception("unable to launch process")
self.procName = cmd[0].split('/')[-1]
if cmd[0] == 'am' and cmd[1] == "instrument":
if cmd[0] == 'am' and cmd[1] in RemoteAutomation._specialAmCommands:
self.procName = app
print "Robocop process name: "+self.procName

View File

@ -8,7 +8,11 @@
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8" />
<uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
#ifdef MOZ_ANDROID_MAX_SDK_VERSION
android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
#endif
android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
<instrumentation
android:name="org.mozilla.gecko.FennecInstrumentationTestRunner"
@ -43,6 +47,14 @@
</activity>
<activity android:name="org.mozilla.gecko.LaunchFennecWithConfigurationActivity"
android:label="Robocop Fennec">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.util.Map;
import org.mozilla.gecko.tests.BaseRobocopTest;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
/**
* An Activity that extracts Robocop settings from robotium.config, launches
* Fennec with the Robocop testing parameters, and finishes itself.
* <p>
* This is intended to be used by local testers using |mach robocop --serve|.
*/
public class LaunchFennecWithConfigurationActivity extends Activity {
@Override
public void onCreate(Bundle arguments) {
super.onCreate(arguments);
}
@Override
public void onResume() {
super.onResume();
final String configFile = FennecNativeDriver.getFile(BaseRobocopTest.DEFAULT_ROOT_PATH + "/robotium.config");
final Map<String, String> config = FennecNativeDriver.convertTextToTable(configFile);
final Intent intent = BaseRobocopTest.createActivityIntent(config);
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
this.finish();
this.startActivity(intent);
}
}

View File

@ -24,6 +24,7 @@ _JAVA_HARNESS := \
FennecTalosAssert.java \
FennecNativeDriver.java \
FennecNativeElement.java \
LaunchFennecWithConfigurationActivity.java \
RoboCopException.java \
RobocopShare1.java \
RobocopShare2.java \

View File

@ -4,7 +4,15 @@
package org.mozilla.gecko.tests;
import java.util.Map;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.test.ActivityInstrumentationTestCase2;
import android.text.TextUtils;
import android.util.Log;
import com.jayway.android.robotium.solo.Solo;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
@ -13,32 +21,32 @@ import org.apache.http.impl.client.DefaultHttpClient;
import org.mozilla.gecko.Actions;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.Driver;
import org.mozilla.gecko.FennecInstrumentationTestRunner;
import org.mozilla.gecko.FennecMochitestAssert;
import org.mozilla.gecko.FennecNativeActions;
import org.mozilla.gecko.FennecNativeDriver;
import org.mozilla.gecko.FennecTalosAssert;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.updater.UpdateServiceHelper;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.PowerManager;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import com.jayway.android.robotium.solo.Solo;
import java.util.Map;
@SuppressWarnings("unchecked")
public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
public static final String LOGTAG = "BaseTest";
public enum Type {
MOCHITEST,
TALOS
}
private static final String DEFAULT_ROOT_PATH = "/mnt/sdcard/tests";
public static final String DEFAULT_ROOT_PATH = "/mnt/sdcard/tests";
// How long to wait for a Robocop:Quit message to actually kill Fennec.
private static final int ROBOCOP_QUIT_WAIT_MS = 180000;
/**
* The Java Class instance that launches the browser.
@ -76,8 +84,6 @@ public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<A
protected StringHelper mStringHelper;
protected abstract Intent createActivityIntent();
/**
* The browser is started at the beginning of this test. A single test is a
* class inheriting from <code>BaseRobocopTest</code> that contains test
@ -112,6 +118,30 @@ public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<A
return Type.MOCHITEST;
}
// Member function to allow specialization.
protected Intent createActivityIntent() {
return BaseRobocopTest.createActivityIntent(mConfig);
}
// Static function to allow re-use.
public static Intent createActivityIntent(Map<String, String> config) {
final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.putExtra("args", "-no-remote -profile " + config.get("profile"));
// Don't show the first run experience.
intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true);
final String envString = config.get("envvars");
if (!TextUtils.isEmpty(envString)) {
final String[] envStrings = envString.split(",");
for (int iter = 0; iter < envStrings.length; iter++) {
intent.putExtra("env" + iter, envStrings[iter]);
}
}
return intent;
}
@Override
protected void setUp() throws Exception {
// Disable the updater.
@ -152,7 +182,43 @@ public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<A
mSolo = new Solo(getInstrumentation(), tempActivity);
mDriver = new FennecNativeDriver(tempActivity, mSolo, mRootPath);
mActions = new FennecNativeActions(tempActivity, mSolo, getInstrumentation(), mAsserter);
}
@Override
public void tearDown() throws Exception {
try {
mAsserter.endTest();
// By default, we don't quit Fennec on finish, and we don't finish
// all opened activities. Not quiting Fennec entirely is intended to
// make life better for local testers, who might want to alter a
// test that is under development rather than Fennec itself. Not
// finishing activities is intended to allow local testers to
// manually inspect an activity's state after a test
// run. runtestsremote.py sets this to "1". Testers running via an
// IDE will not have this set at all.
final String quitAndFinish = FennecInstrumentationTestRunner.getFennecArguments()
.getString("quit_and_finish"); // null means not specified.
if ("1".equals(quitAndFinish)) {
// Request the browser force quit and wait for it to take effect.
Log.i(LOGTAG, "Requesting force quit.");
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null));
mSolo.sleep(ROBOCOP_QUIT_WAIT_MS);
// If still running, finish activities as recommended by Robotium.
Log.i(LOGTAG, "Finishing all opened activities.");
mSolo.finishOpenedActivities();
} else {
// This has the effect of keeping the activity-under-test
// around; if we don't set it to null, it is killed, either by
// finishOpenedActivities above or super.tearDown below.
Log.i(LOGTAG, "Not requesting force quit and trying to keep started activity alive.");
setActivity(null);
}
} catch (Throwable e) {
e.printStackTrace();
}
super.tearDown();
}
/**

View File

@ -144,38 +144,6 @@ abstract class BaseTest extends BaseRobocopTest {
}
}
@Override
public void tearDown() throws Exception {
try {
mAsserter.endTest();
// request a force quit of the browser and wait for it to take effect
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null));
mSolo.sleep(120000);
// if still running, finish activities as recommended by Robotium
mSolo.finishOpenedActivities();
} catch (Throwable e) {
e.printStackTrace();
}
super.tearDown();
}
@Override
protected Intent createActivityIntent() {
final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.putExtra("args", "-no-remote -profile " + mProfile);
final String envString = mConfig.get("envvars");
if (!TextUtils.isEmpty(envString)) {
final String[] envStrings = envString.split(",");
for (int iter = 0; iter < envStrings.length; iter++) {
intent.putExtra("env" + iter, envStrings[iter]);
}
}
return intent;
}
public void assertMatches(String value, String regex, String name) {
if (value == null) {
mAsserter.ok(false, name, "Expected /" + regex + "/, got null");

View File

@ -58,22 +58,6 @@ abstract class UITest extends BaseRobocopTest
throwIfScreenNotOn();
}
@Override
public void tearDown() throws Exception {
try {
mAsserter.endTest();
// request a force quit of the browser and wait for it to take effect
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null));
mSolo.sleep(120000);
// if still running, finish activities as recommended by Robotium
mSolo.finishOpenedActivities();
} catch (Throwable e) {
e.printStackTrace();
}
super.tearDown();
}
@Override
protected void runTest() throws Throwable {
try {
@ -182,26 +166,6 @@ abstract class UITest extends BaseRobocopTest
return baseUrl + "/" + url.replaceAll("(^/)", "");
}
@Override
protected Intent createActivityIntent() {
final Intent intent = new Intent(Intent.ACTION_MAIN);
// Don't show the first run experience.
intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true);
intent.putExtra("args", "-no-remote -profile " + mProfile);
final String envString = mConfig.get("envvars");
if (!TextUtils.isEmpty(envString)) {
final String[] envStrings = envString.split(",");
for (int iter = 0; iter < envStrings.length; iter++) {
intent.putExtra("env" + iter, envStrings[iter]);
}
}
return intent;
}
/**
* Throws an Exception. Called from overridden JUnit methods to ensure JUnit assertions
* are not accidentally used over AssertionHelper assertions (the latter of which contains

View File

@ -580,7 +580,13 @@ class RobocopCommands(MachCommandBase):
help='Test to run. Can be a single Robocop test file (like "testLoad.java") '
' or a directory of tests '
'(to run recursively). If omitted, the entire Robocop suite is run.')
def run_robocop(self, test_paths, **kwargs):
@CommandArgument('--serve', default=False, action='store_true',
help='Run no tests but start the mochi.test web server and launch '
'Fennec with a test profile.')
def run_robocop(self, test_paths, serve=False, **kwargs):
if serve:
kwargs['autorun'] = False
if not kwargs.get('robocopIni'):
kwargs['robocopIni'] = os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'robocop.ini')

View File

@ -543,6 +543,11 @@ def run_test_harness(options):
if (options.dm_trans == 'adb' and options.robocopApk):
dm._checkCmd(["install", "-r", options.robocopApk])
if not options.autorun:
# Force a single loop iteration. The iteration will start Fennec and
# the httpd server, but not actually run a test.
options.testPath = robocop_tests[0]['name']
retVal = None
# Filtering tests
active_tests = []
@ -570,20 +575,36 @@ def run_test_harness(options):
mochitest.localProfile = options.profilePath
options.app = "am"
options.browserArgs = [
"instrument",
"-w",
"-e",
"deviceroot",
deviceRoot,
"-e",
"class"]
options.browserArgs.append(
"org.mozilla.gecko.tests.%s" %
test['name'].split('.java')[0])
options.browserArgs.append(
"org.mozilla.roboexample.test/org.mozilla.gecko.FennecInstrumentationTestRunner")
mochitest.nsprLogName = "nspr-%s.log" % test['name']
if options.autorun:
# This launches a test (using "am instrument") and instructs
# Fennec to /quit/ the browser (using Robocop:Quit) and to
# /finish/ all opened activities.
options.browserArgs = [
"instrument",
"-w",
"-e", "quit_and_finish", "1",
"-e", "deviceroot", deviceRoot,
"-e",
"class"]
options.browserArgs.append(
"org.mozilla.gecko.tests.%s" %
test['name'].split('.java')[0])
options.browserArgs.append(
"org.mozilla.roboexample.test/org.mozilla.gecko.FennecInstrumentationTestRunner")
else:
# This does not launch a test at all. It launches an activity
# that starts Fennec and then waits indefinitely, since cat
# never returns.
options.browserArgs = ["start",
"-n", "org.mozilla.roboexample.test/org.mozilla.gecko.LaunchFennecWithConfigurationActivity",
"&&", "cat"]
dm.default_timeout = sys.maxint # Forever.
mochitest.log.info("")
mochitest.log.info("Serving mochi.test Robocop root at http://%s:%s/tests/robocop/" %
(options.remoteWebServer, options.httpPort))
mochitest.log.info("")
# If the test is for checking the import from bookmarks then make
# sure there is data to import