aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbors-servo <lbergstrom+bors@mozilla.com>2018-07-20 18:53:23 -0400
committerGitHub <noreply@github.com>2018-07-20 18:53:23 -0400
commit37a73d3bc09a8a3f12e4e2d42850419208000ba8 (patch)
treea2832214673949bd250bb297c8bba2a09020c1b3
parentaa76909f55e126957dfc163db9512ffe344c348f (diff)
parent2b5bba6f12481acf315b6c55f3ac129dce373bd6 (diff)
downloadservo-37a73d3bc09a8a3f12e4e2d42850419208000ba8.tar.gz
servo-37a73d3bc09a8a3f12e4e2d42850419208000ba8.zip
Auto merge of #21213 - servo:android-wpt, r=nox+jgraham
Add some support for WPT tests in an Android emulator through WebDriver This succeeds on my machine: `./mach test-wpt --product servodriver --binary etc/run_in_headless_android_emulator.py --binary-arg servo-x86 --binary-arg target/i686-linux-android/release/servo.apk /_mozilla/mozilla/DOMParser.html` <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/21213) <!-- Reviewable:end -->
-rw-r--r--components/constellation/constellation.rs7
-rw-r--r--components/webdriver_server/lib.rs47
-rwxr-xr-xetc/run_in_headless_android_emulator.py124
-rw-r--r--python/servo/testing_commands.py52
-rw-r--r--tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/browsers/servodriver.py22
-rw-r--r--tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/executors/executorservodriver.py80
6 files changed, 256 insertions, 76 deletions
diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs
index c2b14985f3a..e577307cd1b 100644
--- a/components/constellation/constellation.rs
+++ b/components/constellation/constellation.rs
@@ -931,7 +931,12 @@ impl<Message, LTF, STF> Constellation<Message, LTF, STF>
FromCompositorMsg::GetFocusTopLevelBrowsingContext(resp_chan) => {
let focus_browsing_context = self.focus_pipeline_id
.and_then(|pipeline_id| self.pipelines.get(&pipeline_id))
- .map(|pipeline| pipeline.top_level_browsing_context_id);
+ .map(|pipeline| pipeline.top_level_browsing_context_id)
+ .filter(|&top_level_browsing_context_id| {
+ let browsing_context_id =
+ BrowsingContextId::from(top_level_browsing_context_id);
+ self.browsing_contexts.contains_key(&browsing_context_id)
+ });
let _ = resp_chan.send(focus_browsing_context);
}
FromCompositorMsg::KeyEvent(ch, key, state, modifiers) => {
diff --git a/components/webdriver_server/lib.rs b/components/webdriver_server/lib.rs
index 150a3001c71..47390248dd3 100644
--- a/components/webdriver_server/lib.rs
+++ b/components/webdriver_server/lib.rs
@@ -107,14 +107,14 @@ struct WebDriverSession {
/// Time to wait for injected scripts to run before interrupting them. A [`None`] value
/// specifies that the script should run indefinitely.
- script_timeout: Option<u64>,
+ script_timeout: u64,
/// Time to wait for a page to finish loading upon navigation.
- load_timeout: Option<u64>,
+ load_timeout: u64,
/// Time to wait for the element location strategy when retrieving elements, and when
/// waiting for an element to become interactable.
- implicit_wait_timeout: Option<u64>,
+ implicit_wait_timeout: u64,
}
impl WebDriverSession {
@@ -127,9 +127,9 @@ impl WebDriverSession {
browsing_context_id: browsing_context_id,
top_level_browsing_context_id: top_level_browsing_context_id,
- script_timeout: Some(30_000),
- load_timeout: Some(300_000),
- implicit_wait_timeout: Some(0),
+ script_timeout: 30_000,
+ load_timeout: 300_000,
+ implicit_wait_timeout: 0,
}
}
}
@@ -140,7 +140,7 @@ struct Handler {
resize_timeout: u32,
}
-#[derive(Clone, Copy, PartialEq)]
+#[derive(Clone, Copy, Debug, PartialEq)]
enum ServoExtensionRoute {
GetPrefs,
SetPrefs,
@@ -171,7 +171,7 @@ impl WebDriverExtensionRoute for ServoExtensionRoute {
}
}
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
enum ServoExtensionCommand {
GetPrefs(GetPrefsParameters),
SetPrefs(SetPrefsParameters),
@@ -188,7 +188,7 @@ impl WebDriverExtensionCommand for ServoExtensionCommand {
}
}
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
struct GetPrefsParameters {
prefs: Vec<String>
}
@@ -222,7 +222,7 @@ impl ToJson for GetPrefsParameters {
}
}
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
struct SetPrefsParameters {
prefs: Vec<(String, PrefValue)>
}
@@ -371,7 +371,7 @@ impl Handler {
-> WebDriverResult<WebDriverResponse> {
let timeout = self.session()?.load_timeout;
thread::spawn(move || {
- thread::sleep(Duration::from_millis(timeout.unwrap()));
+ thread::sleep(Duration::from_millis(timeout));
let _ = sender.send(LoadStatus::LoadTimeout);
});
@@ -720,9 +720,15 @@ impl Handler {
.as_mut()
.ok_or(WebDriverError::new(ErrorStatus::SessionNotCreated, ""))?;
- session.script_timeout = parameters.script;
- session.load_timeout = parameters.page_load;
- session.implicit_wait_timeout = parameters.implicit;
+ if let Some(timeout) = parameters.script {
+ session.script_timeout = timeout
+ }
+ if let Some(timeout) = parameters.page_load {
+ session.load_timeout = timeout
+ }
+ if let Some(timeout) = parameters.implicit {
+ session.implicit_wait_timeout = timeout
+ }
Ok(WebDriverResponse::Void)
}
@@ -750,15 +756,10 @@ impl Handler {
let func_body = &parameters.script;
let args_string = "window.webdriverCallback";
- let script = match self.session()?.script_timeout {
- Some(timeout) => {
- format!("setTimeout(webdriverTimeout, {}); (function(callback) {{ {} }})({})",
- timeout,
- func_body,
- args_string)
- }
- None => format!("(function(callback) {{ {} }})({})", func_body, args_string),
- };
+ let script = format!("setTimeout(webdriverTimeout, {}); (function(callback) {{ {} }})({})",
+ self.session()?.script_timeout,
+ func_body,
+ args_string);
let (sender, receiver) = ipc::channel().unwrap();
let command = WebDriverScriptCommand::ExecuteAsyncScript(script, sender);
diff --git a/etc/run_in_headless_android_emulator.py b/etc/run_in_headless_android_emulator.py
index 45e088e0389..32f8e18146f 100755
--- a/etc/run_in_headless_android_emulator.py
+++ b/etc/run_in_headless_android_emulator.py
@@ -48,33 +48,30 @@ def main(avd_name, apk_path, *args):
# Now `adb shell` will work, but `adb install` needs a system service
# that might still be in the midle of starting and not be responsive yet.
+ wait_for_boot(adb)
- # https://stackoverflow.com/a/38896494/1162888
- while 1:
- with terminate_on_exit(
- adb + ["shell", "getprop", "sys.boot_completed"],
- stdout=subprocess.PIPE,
- ) as getprop:
- stdout, stderr = getprop.communicate()
- if "1" in stdout:
- break
- time.sleep(1)
-
+ # These steps should happen before application start
check_call(adb + ["install", "-r", apk_path])
-
- data_dir = "/sdcard/Android/data/com.mozilla.servo/files"
- params_file = data_dir + "/android_params"
-
- check_call(adb + ["shell", "mkdir -p %s" % data_dir])
- check_call(adb + ["shell", "echo 'servo' > %s" % params_file])
- for arg in args:
- check_call(adb + ["shell", "echo %s >> %s" % (shell_quote(arg), params_file)])
+ args = list(args)
+ write_user_stylesheets(adb, args)
+ write_args(adb, args)
check_call(adb + ["shell", "am start com.mozilla.servo/com.mozilla.servo.MainActivity"],
stdout=sys.stderr)
- logcat_args = ["RustAndroidGlueStdouterr:D", "*:S", "-v", "raw"]
+ # Start showing logs as soon as the application starts,
+ # in case they say something useful while we wait in subsequent steps.
+ logcat_args = [
+ "--format=raw", # Print no metadata, only log messages
+ "RustAndroidGlueStdouterr:D", # Show (debug level) Rust stdio
+ "*:S", # Hide everything else
+ ]
with terminate_on_exit(adb + ["logcat"] + logcat_args) as logcat:
+
+ # This step needs to happen after application start
+ forward_webdriver(adb, args)
+
+ # logcat normally won't exit on its own, wait until we get a SIGTERM signal.
logcat.wait()
@@ -103,11 +100,90 @@ def terminate_on_exit(*args, **kwargs):
process.terminate()
-def check_call(*args, **kwargs):
+# https://stackoverflow.com/a/38896494/1162888
+def wait_for_boot(adb):
+ while 1:
+ with terminate_on_exit(
+ adb + ["shell", "getprop", "sys.boot_completed"],
+ stdout=subprocess.PIPE,
+ ) as getprop:
+ stdout, stderr = getprop.communicate()
+ if "1" in stdout:
+ return
+ time.sleep(1)
+
+
+def call(*args, **kwargs):
with terminate_on_exit(*args, **kwargs) as process:
- exit_code = process.wait()
- if exit_code != 0:
- sys.exit(exit_code)
+ return process.wait()
+
+
+def check_call(*args, **kwargs):
+ exit_code = call(*args, **kwargs)
+ if exit_code != 0:
+ sys.exit(exit_code)
+
+
+def write_args(adb, args):
+ data_dir = "/sdcard/Android/data/com.mozilla.servo/files"
+ params_file = data_dir + "/android_params"
+
+ check_call(adb + ["shell", "mkdir -p %s" % data_dir])
+ check_call(adb + ["shell", "echo 'servo' > %s" % params_file])
+ for arg in args:
+ check_call(adb + ["shell", "echo %s >> %s" % (shell_quote(arg), params_file)])
+
+
+def write_user_stylesheets(adb, args):
+ data_dir = "/sdcard/Android/data/com.mozilla.servo/files"
+ check_call(adb + ["shell", "mkdir -p %s" % data_dir])
+ for i, (pos, path) in enumerate(extract_args("--user-stylesheet", args)):
+ remote_path = "%s/user%s.css" % (data_dir, i)
+ args[pos] = remote_path
+ check_call(adb + ["push", path, remote_path], stdout=sys.stderr)
+
+
+def forward_webdriver(adb, args):
+ webdriver_port = extract_arg("--webdriver", args)
+ if webdriver_port is not None:
+ # `adb forward` will start accepting TCP connections even if the other side does not.
+ # (If the remote side refuses the connection,
+ # adb will close the local side after accepting it.)
+ # This is incompatible with wptrunner which relies on TCP connection acceptance
+ # to figure out when it can start sending WebDriver requests.
+ #
+ # So wait until the remote side starts listening before setting up the forwarding.
+ wait_for_tcp_server(adb, webdriver_port)
+
+ port = "tcp:%s" % webdriver_port
+ check_call(adb + ["forward", port, port])
+ sys.stderr.write("Forwarding WebDriver port %s to the emulator\n" % webdriver_port)
+
+ split = os.environ.get("EMULATOR_REVERSE_FORWARD_PORTS", "").split(",")
+ ports = [int(part) for part in split if part]
+ for port in ports:
+ port = "tcp:%s" % port
+ check_call(adb + ["reverse", port, port])
+ if ports:
+ sys.stderr.write("Reverse-forwarding ports %s\n" % ", ".join(map(str, ports)))
+
+
+def extract_arg(name, args):
+ for _, arg in extract_args(name, args):
+ return arg
+
+
+def extract_args(name, args):
+ previous_arg_matches = False
+ for i, arg in enumerate(args):
+ if previous_arg_matches:
+ yield i, arg
+ previous_arg_matches = arg == name
+
+
+def wait_for_tcp_server(adb, port):
+ while call(adb + ["shell", "nc -z 127.0.0.1 %s" % port], stdout=sys.stderr) != 0:
+ time.sleep(1)
# Copied from Python 3.3+'s shlex.quote()
diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py
index f4092e702be..b0ef2cfa5ce 100644
--- a/python/servo/testing_commands.py
+++ b/python/servo/testing_commands.py
@@ -367,6 +367,21 @@ class MachCommands(CommandBase):
else:
return ret
+ @Command('test-wpt-android',
+ description='Run the web platform test suite in an Android emulator',
+ category='testing',
+ parser=create_parser_wpt)
+ def test_wpt_android(self, release=False, dev=False, binary_args=None, **kwargs):
+ kwargs.update(
+ release=release,
+ dev=dev,
+ product="servodriver",
+ processes=1,
+ binary_args=self.in_android_emulator(release, dev) + (binary_args or []),
+ binary=sys.executable,
+ )
+ return self._test_wpt(**kwargs)
+
def _test_wpt(self, **kwargs):
hosts_file_path = path.join(self.context.topdir, 'tests', 'wpt', 'hosts')
os.environ["hosts_file_path"] = hosts_file_path
@@ -559,28 +574,15 @@ class MachCommands(CommandBase):
@CommandArgument('--dev', '-d', action='store_true',
help='Run the dev build')
def test_android_startup(self, release, dev):
- if (release and dev) or not (release or dev):
- print("Please specify one of --dev or --release.")
- return 1
-
- avd = "servo-x86"
- target = "i686-linux-android"
- print("Assuming --target " + target)
-
- env = self.build_env(target=target)
- assert self.handle_android_target(target)
- binary_path = self.get_binary_path(release, dev, android=True)
- apk = binary_path + ".apk"
-
html = """
<script>
console.log("JavaScript is running!")
</script>
"""
url = "data:text/html;base64," + html.encode("base64").replace("\n", "")
- py = path.join(self.context.topdir, "etc", "run_in_headless_android_emulator.py")
- args = [sys.executable, py, avd, apk, url]
- process = subprocess.Popen(args, stdout=subprocess.PIPE, env=env)
+ args = self.in_android_emulator(release, dev)
+ args = [sys.executable] + args + [url]
+ process = subprocess.Popen(args, stdout=subprocess.PIPE)
try:
while 1:
line = process.stdout.readline()
@@ -593,6 +595,24 @@ class MachCommands(CommandBase):
finally:
process.terminate()
+ def in_android_emulator(self, release, dev):
+ if (release and dev) or not (release or dev):
+ print("Please specify one of --dev or --release.")
+ sys.exit(1)
+
+ avd = "servo-x86"
+ target = "i686-linux-android"
+ print("Assuming --target " + target)
+
+ env = self.build_env(target=target)
+ os.environ["PATH"] = env["PATH"]
+ assert self.handle_android_target(target)
+ binary_path = self.get_binary_path(release, dev, android=True)
+ apk = binary_path + ".apk"
+
+ py = path.join(self.context.topdir, "etc", "run_in_headless_android_emulator.py")
+ return [py, avd, apk]
+
@Command('test-jquery',
description='Run the jQuery test suite',
category='testing')
diff --git a/tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/browsers/servodriver.py b/tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/browsers/servodriver.py
index 168a576c25a..4111d8b48a8 100644
--- a/tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/browsers/servodriver.py
+++ b/tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/browsers/servodriver.py
@@ -37,7 +37,9 @@ def check_args(**kwargs):
def browser_kwargs(test_type, run_info_data, **kwargs):
return {
"binary": kwargs["binary"],
+ "binary_args": kwargs["binary_args"],
"debug_info": kwargs["debug_info"],
+ "server_config": kwargs["config"],
"user_stylesheets": kwargs.get("user_stylesheets"),
}
@@ -71,16 +73,19 @@ def write_hosts_file(config):
class ServoWebDriverBrowser(Browser):
used_ports = set()
+ init_timeout = 300 # Large timeout for cases where we're booting an Android emulator
def __init__(self, logger, binary, debug_info=None, webdriver_host="127.0.0.1",
- user_stylesheets=None):
+ server_config=None, binary_args=None, user_stylesheets=None):
Browser.__init__(self, logger)
self.binary = binary
+ self.binary_args = binary_args or []
self.webdriver_host = webdriver_host
self.webdriver_port = None
self.proc = None
self.debug_info = debug_info
- self.hosts_path = write_hosts_file()
+ self.hosts_path = write_hosts_file(server_config)
+ self.server_ports = server_config.ports if server_config else {}
self.command = None
self.user_stylesheets = user_stylesheets if user_stylesheets else []
@@ -91,10 +96,16 @@ class ServoWebDriverBrowser(Browser):
env = os.environ.copy()
env["HOST_FILE"] = self.hosts_path
env["RUST_BACKTRACE"] = "1"
+ env["EMULATOR_REVERSE_FORWARD_PORTS"] = ",".join(
+ str(port)
+ for _protocol, ports in self.server_ports.items()
+ for port in ports
+ if port
+ )
debug_args, command = browser_command(
self.binary,
- [
+ self.binary_args + [
"--hard-fail",
"--webdriver", str(self.webdriver_port),
"about:blank",
@@ -151,9 +162,10 @@ class ServoWebDriverBrowser(Browser):
def cleanup(self):
self.stop()
- shutil.rmtree(os.path.dirname(self.hosts_file))
+ os.remove(self.hosts_path)
def executor_browser(self):
assert self.webdriver_port is not None
return ExecutorBrowser, {"webdriver_host": self.webdriver_host,
- "webdriver_port": self.webdriver_port}
+ "webdriver_port": self.webdriver_port,
+ "init_timeout": self.init_timeout}
diff --git a/tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/executors/executorservodriver.py b/tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/executors/executorservodriver.py
index f649a75cd93..896aeb39eff 100644
--- a/tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/executors/executorservodriver.py
+++ b/tests/wpt/web-platform-tests/tools/wptrunner/wptrunner/executors/executorservodriver.py
@@ -6,13 +6,16 @@ import time
import traceback
from .base import (Protocol,
+ BaseProtocolPart,
RefTestExecutor,
RefTestImplementation,
TestharnessExecutor,
strip_server)
from ..testrunner import Stop
+from ..webdriver_server import wait_for_service
webdriver = None
+ServoCommandExtensions = None
here = os.path.join(os.path.split(__file__)[0])
@@ -23,20 +26,76 @@ def do_delayed_imports():
global webdriver
import webdriver
+ global ServoCommandExtensions
+ class ServoCommandExtensions(object):
+ def __init__(self, session):
+ self.session = session
+
+ @webdriver.client.command
+ def get_prefs(self, *prefs):
+ body = {"prefs": list(prefs)}
+ return self.session.send_session_command("POST", "servo/prefs/get", body)
+
+ @webdriver.client.command
+ def set_prefs(self, prefs):
+ body = {"prefs": prefs}
+ return self.session.send_session_command("POST", "servo/prefs/set", body)
+
+ @webdriver.client.command
+ def reset_prefs(self, *prefs):
+ body = {"prefs": list(prefs)}
+ return self.session.send_session_command("POST", "servo/prefs/reset", body)
+
+ def change_prefs(self, old_prefs, new_prefs):
+ # Servo interprets reset with an empty list as reset everything
+ if old_prefs:
+ self.reset_prefs(*old_prefs.keys())
+ self.set_prefs({k: parse_pref_value(v) for k, v in new_prefs.items()})
+
+
+# See parse_pref_from_command_line() in components/config/opts.rs
+def parse_pref_value(value):
+ if value == "true":
+ return True
+ if value == "false":
+ return False
+ try:
+ return float(value)
+ except ValueError:
+ return value
+
+
+class ServoBaseProtocolPart(BaseProtocolPart):
+ def execute_script(self, script, async=False):
+ pass
+
+ def set_timeout(self, timeout):
+ pass
+
+ def wait(self):
+ pass
+
+ def set_window(self, handle):
+ pass
+
class ServoWebDriverProtocol(Protocol):
+ implements = [ServoBaseProtocolPart]
+
def __init__(self, executor, browser, capabilities, **kwargs):
do_delayed_imports()
Protocol.__init__(self, executor, browser)
self.capabilities = capabilities
self.host = browser.webdriver_host
self.port = browser.webdriver_port
+ self.init_timeout = browser.init_timeout
self.session = None
def connect(self):
"""Connect to browser via WebDriver."""
- self.session = webdriver.Session(self.host, self.port,
- extension=webdriver.servo.ServoCommandExtensions)
+ wait_for_service((self.host, self.port), timeout=self.init_timeout)
+
+ self.session = webdriver.Session(self.host, self.port, extension=ServoCommandExtensions)
self.session.start()
def after_connect(self):
@@ -70,11 +129,6 @@ class ServoWebDriverProtocol(Protocol):
self.logger.error(traceback.format_exc(e))
break
- def on_environment_change(self, old_environment, new_environment):
- #Unset all the old prefs
- self.session.extension.reset_prefs(*old_environment.get("prefs", {}).keys())
- self.session.extension.set_prefs(new_environment.get("prefs", {}))
-
class ServoWebDriverRun(object):
def __init__(self, func, session, url, timeout, current_timeout=None):
@@ -174,6 +228,12 @@ class ServoWebDriverTestharnessExecutor(TestharnessExecutor):
session.back()
return result
+ def on_environment_change(self, new_environment):
+ self.protocol.session.extension.change_prefs(
+ self.last_environment.get("prefs", {}),
+ new_environment.get("prefs", {})
+ )
+
class TimeoutError(Exception):
pass
@@ -240,3 +300,9 @@ class ServoWebDriverRefTestExecutor(RefTestExecutor):
session.url = url
session.execute_async_script(self.wait_script)
return session.screenshot()
+
+ def on_environment_change(self, new_environment):
+ self.protocol.session.extension.change_prefs(
+ self.last_environment.get("prefs", {}),
+ new_environment.get("prefs", {})
+ )