Bug 870908 - Add Robocop Java harness for writing tests in Javascript. r=rnewman

This commit is contained in:
Nick Alexander 2013-05-10 19:45:58 -07:00
parent 2d0fd713b6
commit b6eec5394e
6 changed files with 1412 additions and 0 deletions

View File

@ -71,6 +71,8 @@ MOCHITEST_ROBOCOP_FILES := \
$(wildcard $(TESTPATH)/*.html) \
$(wildcard $(TESTPATH)/*.jpg) \
$(wildcard $(TESTPATH)/*.sjs) \
$(wildcard $(TESTPATH)/test*.js) \
$(wildcard $(TESTPATH)/robocop*.js) \
$(NULL)
GARBAGE += \

View File

@ -232,11 +232,38 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
contentEventExpecter.unregisterListener();
}
/**
* Load <code>url</code> using the awesome bar UI and sending key strokes.
*
* This method waits synchronously for the <code>DOMContentLoaded</code>
* message from Gecko before returning.
*/
protected final void loadUrl(String url) {
enterUrl(url);
hitEnterAndWait();
}
/**
* Load <code>url</code> using reflection and the internal
* <code>org.mozilla.gecko.Tabs</code> API.
*
* This method does not wait for any confirmation from Gecko before
* returning.
*/
protected final void loadUrlInTab(final String url) {
try {
ClassLoader classLoader = getActivity().getClassLoader();
Class tabsClass = classLoader.loadClass("org.mozilla.gecko.Tabs");
Method getInstance = tabsClass.getMethod("getInstance");
Method loadUrlInTab = tabsClass.getMethod("loadUrlInTab", String.class);
Object tabs = getInstance.invoke(null);
loadUrlInTab.invoke(tabs, new Object[] { url });
} catch (Exception e) {
mAsserter.dumpLog("Exception in loadUrlInTab", e);
throw new RuntimeException(e);
}
}
public final void verifyUrl(String url) {
Activity awesomeBarActivity = clickOnAwesomeBar();
Element urlbar = mDriver.findElement(awesomeBarActivity, "awesomebar_text");

View File

@ -0,0 +1,148 @@
#filter substitution
package @ANDROID_PACKAGE_NAME@.tests;
import @ANDROID_PACKAGE_NAME@.*;
import android.util.Log;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import junit.framework.AssertionFailedError;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class JavascriptTest extends BaseTest {
public static final String LOGTAG = "JavascriptTest";
public final String javascriptUrl;
public JavascriptTest(String javascriptUrl) {
super();
this.javascriptUrl = javascriptUrl;
}
@Override
protected int getTestType() {
return TEST_MOCHITEST;
}
/**
* Route messages from Javascript's head.js test framework into Java's
* Mochitest framework.
*/
protected static class JavascriptTestMessageParser {
// Messages matching this pattern are handled specially. Messages not
// matching this pattern are still printed.
private static final Pattern testMessagePattern =
Pattern.compile("\n+TEST-(.*) \\| (.*) \\| (.*)\n*");
private final Assert mAsserter;
// Used to help print stack traces neatly.
private String lastTestName = "";
// Have we seen a message saying the test is finished?
private boolean testFinishedMessageSeen = false;
public JavascriptTestMessageParser(final Assert asserter) {
this.mAsserter = asserter;
}
private boolean testIsFinished() {
return testFinishedMessageSeen;
}
private void logMessage(String str) {
Matcher m = testMessagePattern.matcher(str);
if (m.matches()) {
String type = m.group(1);
String name = m.group(2);
String message = m.group(3);
if ("INFO".equals(type)) {
mAsserter.info(name, message);
testFinishedMessageSeen = testFinishedMessageSeen ||
"exiting test".equals(message);
} else if ("PASS".equals(type)) {
mAsserter.ok(true, name, message);
} else if ("UNEXPECTED-FAIL".equals(type)) {
try {
mAsserter.ok(false, name, message);
} catch (junit.framework.AssertionFailedError e) {
// Swallow this exception. We want to see all the
// Javascript failures, not die on the very first one!
}
} else if ("KNOWN-FAIL".equals(type)) {
mAsserter.todo(false, name, message);
} else if ("UNEXPECTED-PASS".equals(type)) {
mAsserter.todo(true, name, message);
}
lastTestName = name;
} else {
// Generally, these extra lines are stack traces from failures,
// so we print them with the name of the last test seen.
mAsserter.info(lastTestName, str.trim());
}
}
}
public void testJavascript() throws Exception {
blockForGeckoReady();
// We want to be waiting for Robocop messages before the page is loaded
// because the test harness runs each test in the suite (and possibly
// completes testing) before the page load event is fired.
final Actions.EventExpecter expecter = mActions.expectGeckoEvent("Robocop:Status");
mAsserter.dumpLog("Registered listener for Robocop:Status");
final String url = getAbsoluteUrl("/robocop/robocop_javascript.html?path=" + javascriptUrl);
mAsserter.dumpLog("Loading JavaScript test from " + url);
loadUrlInTab(url);
final JavascriptTestMessageParser testMessageParser =
new JavascriptTestMessageParser(mAsserter);
try {
while (true) {
if (Log.isLoggable(LOGTAG, Log.VERBOSE)) {
Log.v(LOGTAG, "Waiting for Robocop:Status");
}
String data = expecter.blockForEventData();
if (Log.isLoggable(LOGTAG, Log.VERBOSE)) {
Log.v(LOGTAG, "Got Robocop:Status with data '" + data + "'");
}
JSONObject o = new JSONObject(data);
String innerType = o.getString("innerType");
if (!"progress".equals(innerType)) {
throw new Exception("Unexpected Robocop:Status innerType " + innerType);
}
String message = o.getString("message");
if (message == null) {
throw new Exception("Robocop:Status progress message must not be null");
}
testMessageParser.logMessage(message);
if (testMessageParser.testIsFinished()) {
if (Log.isLoggable(LOGTAG, Log.DEBUG)) {
Log.d(LOGTAG, "Got test finished message");
}
break;
}
}
} finally {
expecter.unregisterListener();
mAsserter.dumpLog("Unregistered listener for Robocop:Status");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>Mochitest Robotium Javascript Test Harness</title>
<link rel="author" title="nalexander" href="mailto:nalexander@mozilla.com">
<script type="application/javascript;version=1.8" src="robocop_testharness.js"></script>
<script>
var param = /[&?]path=([^&]+)/.exec(location.search);
if (param) {
// We encode so that absolute URLs can be provided. Since the
// encoding of a relative filename is just the filename, no special
// processing is needed for the most common case.
var src = decodeURIComponent(param[1]);
document.title = src;
// Provided by robocop_testharness.js.
testOneFile(src);
}
</script>
</head>

View File

@ -0,0 +1,74 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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/. */
let bridge = SpecialPowers.Cc["@mozilla.org/android/bridge;1"]
.getService(SpecialPowers.Ci.nsIAndroidBridge);
function sendMessageToJava(message) {
let data = JSON.stringify(message);
bridge.handleGeckoMessage(data);
}
function _evalURI(uri, sandbox) {
// We explicitly allow Cross-Origin requests, since it is useful for
// testing, but we allow relative URLs by maintaining our baseURI.
let req = SpecialPowers.Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
let baseURI = SpecialPowers.Services.io
.newURI(window.document.baseURI, window.document.characterSet, null);
let theURI = SpecialPowers.Services.io
.newURI(uri, window.document.characterSet, baseURI);
req.open('GET', theURI.spec, false);
req.send();
return SpecialPowers.Cu.evalInSandbox(req.responseText, sandbox, "1.8", uri, 1);
}
/**
* Execute the Javascript file at `uri` in a testing sandbox populated
* with the Javascript test harness.
*
* `uri` should be a String, relative (to window.document.baseURI) or
* absolute.
*
* The Javascript test harness sends all output to Java via
* Robocop:Status messages.
*/
function testOneFile(uri) {
let HEAD_JS = "robocop_head.js";
// System principal. This is dangerous, but this is test code that
// should only run on developer and build farm machines, and the
// test harness needs access to a lot of the Components API,
// including Components.stack. Wrapping Components.stack in
// SpecialPowers magic obfuscates stack traces wonderfully,
// defeating much of the point of the test harness.
let principal = SpecialPowers.Cc["@mozilla.org/systemprincipal;1"]
.createInstance(SpecialPowers.Ci.nsIPrincipal);
let testScope = SpecialPowers.Cu.Sandbox(principal);
// Populate test environment with test harness prerequisites.
testScope.Components = SpecialPowers.Components;
testScope._TEST_FILE = uri;
// Output from head.js is fed, line by line, to this function. We
// send any such output back to the Java Robocop harness.
testScope.dump = function (str) {
let message = { type: "Robocop:Status",
innerType: "progress",
message: str,
};
sendMessageToJava(message);
};
// Populate test environment with test harness. The symbols defined
// above must be present before executing the test harness.
_evalURI(HEAD_JS, testScope);
return _evalURI(uri, testScope);
}