diff options
author | bors-servo <lbergstrom+bors@mozilla.com> | 2018-07-20 18:53:23 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-20 18:53:23 -0400 |
commit | 37a73d3bc09a8a3f12e4e2d42850419208000ba8 (patch) | |
tree | a2832214673949bd250bb297c8bba2a09020c1b3 | |
parent | aa76909f55e126957dfc163db9512ffe344c348f (diff) | |
parent | 2b5bba6f12481acf315b6c55f3ac129dce373bd6 (diff) | |
download | servo-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 -->
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 = ¶meters.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", {}) + ) |