diff --git a/tests/wpt/harness/requirements.txt b/tests/wpt/harness/requirements.txt index d6f528cc3fd..0981fb8fe96 100644 --- a/tests/wpt/harness/requirements.txt +++ b/tests/wpt/harness/requirements.txt @@ -1,5 +1,4 @@ html5lib >= 0.99 mozinfo >= 0.7 mozlog >= 2.8 -# Unfortunately, just for gdb flags -mozrunner >= 6.1 +mozdebug >= 0.1 diff --git a/tests/wpt/harness/requirements_firefox.txt b/tests/wpt/harness/requirements_firefox.txt index d3562bdc5ca..379e522acc4 100644 --- a/tests/wpt/harness/requirements_firefox.txt +++ b/tests/wpt/harness/requirements_firefox.txt @@ -1,4 +1,5 @@ -marionette_client >= 0.7.10 +marionette_driver >= 0.4 mozprofile >= 0.21 mozprocess >= 0.19 mozcrash >= 0.13 +mozrunner >= 6.7 diff --git a/tests/wpt/harness/test/metadata/testharness/firefox/test_pref_set.html.ini b/tests/wpt/harness/test/metadata/testharness/firefox/test_pref_set.html.ini new file mode 100644 index 00000000000..bc9bfb9c413 --- /dev/null +++ b/tests/wpt/harness/test/metadata/testharness/firefox/test_pref_set.html.ini @@ -0,0 +1,3 @@ +[test_pref_set.html] + prefs: ["browser.display.foreground_color:#00FF00", + "browser.display.background_color:#000000"] diff --git a/tests/wpt/harness/test/test.py b/tests/wpt/harness/test/test.py index bca2c106132..234cb671353 100644 --- a/tests/wpt/harness/test/test.py +++ b/tests/wpt/harness/test/test.py @@ -139,7 +139,7 @@ def get_parser(): help="Specific product to include in test run") parser.add_argument("--pdb", action="store_true", help="Invoke pdb on uncaught exception") - parser.add_argument("test", nargs="*", type=wptcommandline.slash_prefixed, + parser.add_argument("test", nargs="*", help="Specific tests to include in test run") return parser diff --git a/tests/wpt/harness/test/testdata/testharness/firefox/test_pref_set.html b/tests/wpt/harness/test/testdata/testharness/firefox/test_pref_set.html new file mode 100644 index 00000000000..9a783ed78c9 --- /dev/null +++ b/tests/wpt/harness/test/testdata/testharness/firefox/test_pref_set.html @@ -0,0 +1,10 @@ + +
Test requires the pref browser.display.foreground_color to be set to #00FF00
+ \ No newline at end of file diff --git a/tests/wpt/harness/wptrunner/browsers/b2g.py b/tests/wpt/harness/wptrunner/browsers/b2g.py index 1c3c1397cf2..e642897f158 100644 --- a/tests/wpt/harness/wptrunner/browsers/b2g.py +++ b/tests/wpt/harness/wptrunner/browsers/b2g.py @@ -20,6 +20,7 @@ from mozprofile import FirefoxProfile, Preferences from .base import get_free_port, BrowserError, Browser, ExecutorBrowser from ..executors.executormarionette import MarionetteTestharnessExecutor from ..hosts import HostsFile, HostsLine +from ..environment import hostnames here = os.path.split(__file__)[0] @@ -115,13 +116,6 @@ class B2GBrowser(Browser): self.logger.debug("Device runner started") def setup_hosts(self): - hostnames = ["web-platform.test", - "www.web-platform.test", - "www1.web-platform.test", - "www2.web-platform.test", - "xn--n8j6ds53lwwkrqhv28a.web-platform.test", - "xn--lve-6lad.web-platform.test"] - host_ip = moznetwork.get_ip() temp_dir = tempfile.mkdtemp() diff --git a/tests/wpt/harness/wptrunner/browsers/b2g_setup/certtest_app.zip b/tests/wpt/harness/wptrunner/browsers/b2g_setup/certtest_app.zip index ade8879b212..f9cbd5300ad 100644 Binary files a/tests/wpt/harness/wptrunner/browsers/b2g_setup/certtest_app.zip and b/tests/wpt/harness/wptrunner/browsers/b2g_setup/certtest_app.zip differ diff --git a/tests/wpt/harness/wptrunner/browsers/base.py b/tests/wpt/harness/wptrunner/browsers/base.py index b7d3d0502b8..1d3b3d231c7 100644 --- a/tests/wpt/harness/wptrunner/browsers/base.py +++ b/tests/wpt/harness/wptrunner/browsers/base.py @@ -41,6 +41,18 @@ def get_free_port(start_port, exclude=None): finally: s.close() +def browser_command(binary, args, debug_info): + if debug_info: + if debug_info.requiresEscapedArgs: + args = [item.replace("&", "\\&") for item in args] + debug_args = [debug_info.path] + debug_info.args + else: + debug_args = [] + + command = [binary] + args + + return debug_args, command + class BrowserError(Exception): pass diff --git a/tests/wpt/harness/wptrunner/browsers/firefox.py b/tests/wpt/harness/wptrunner/browsers/firefox.py index ced31e724be..0f009494e92 100644 --- a/tests/wpt/harness/wptrunner/browsers/firefox.py +++ b/tests/wpt/harness/wptrunner/browsers/firefox.py @@ -12,9 +12,10 @@ from mozprofile.permissions import ServerLocations from mozrunner import FirefoxRunner from mozcrash import mozcrash -from .base import get_free_port, Browser, ExecutorBrowser, require_arg, cmd_arg +from .base import get_free_port, Browser, ExecutorBrowser, require_arg, cmd_arg, browser_command from ..executors import executor_kwargs as base_executor_kwargs from ..executors.executormarionette import MarionetteTestharnessExecutor, MarionetteRefTestExecutor +from ..environment import hostnames here = os.path.join(os.path.split(__file__)[0]) @@ -37,8 +38,7 @@ def check_args(**kwargs): def browser_kwargs(**kwargs): return {"binary": kwargs["binary"], "prefs_root": kwargs["prefs_root"], - "debug_args": kwargs["debug_args"], - "interactive": kwargs["interactive"], + "debug_info": kwargs["debug_info"], "symbols_path": kwargs["symbols_path"], "stackwalk_binary": kwargs["stackwalk_binary"], "certutil_binary": kwargs["certutil_binary"], @@ -57,13 +57,13 @@ def env_options(): "external_host": "web-platform.test", "bind_hostname": "false", "certificate_domain": "web-platform.test", - "encrypt_after_connect": True} + "supports_debugger": True} class FirefoxBrowser(Browser): used_ports = set() - def __init__(self, logger, binary, prefs_root, debug_args=None, interactive=None, + def __init__(self, logger, binary, prefs_root, debug_info=None, symbols_path=None, stackwalk_binary=None, certutil_binary=None, ca_certificate_path=None): Browser.__init__(self, logger) @@ -72,8 +72,7 @@ class FirefoxBrowser(Browser): self.marionette_port = None self.used_ports.add(self.marionette_port) self.runner = None - self.debug_args = debug_args - self.interactive = interactive + self.debug_info = debug_info self.profile = None self.symbols_path = symbols_path self.stackwalk_binary = stackwalk_binary @@ -84,38 +83,35 @@ class FirefoxBrowser(Browser): self.marionette_port = get_free_port(2828, exclude=self.used_ports) env = os.environ.copy() - env["MOZ_CRASHREPORTER"] = "1" - env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1" - env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" locations = ServerLocations(filename=os.path.join(here, "server-locations.txt")) preferences = self.load_prefs() - ports = {"http": "8000", - "https": "8443", - "ws": "8888"} - self.profile = FirefoxProfile(locations=locations, - proxy=ports, preferences=preferences) self.profile.set_preferences({"marionette.defaultPrefs.enabled": True, "marionette.defaultPrefs.port": self.marionette_port, - "dom.disable_open_during_load": False}) + "dom.disable_open_during_load": False, + "network.dns.localDomains": ",".join(hostnames)}) if self.ca_certificate_path is not None: self.setup_ssl() + debug_args, cmd = browser_command(self.binary, [cmd_arg("marionette"), "about:blank"], + self.debug_info) + self.runner = FirefoxRunner(profile=self.profile, - binary=self.binary, - cmdargs=[cmd_arg("marionette"), "about:blank"], + binary=cmd[0], + cmdargs=cmd[1:], env=env, process_class=ProcessHandler, process_args={"processOutputLine": [self.on_output]}) self.logger.debug("Starting Firefox") - self.runner.start(debug_args=self.debug_args, interactive=self.interactive) + + self.runner.start(debug_args=debug_args, interactive=self.debug_info and self.debug_info.interactive) self.logger.debug("Firefox Started") def load_prefs(self): diff --git a/tests/wpt/harness/wptrunner/browsers/servo.py b/tests/wpt/harness/wptrunner/browsers/servo.py index 988cb2b5479..11499b66b9f 100644 --- a/tests/wpt/harness/wptrunner/browsers/servo.py +++ b/tests/wpt/harness/wptrunner/browsers/servo.py @@ -26,8 +26,7 @@ def check_args(**kwargs): def browser_kwargs(**kwargs): return {"binary": kwargs["binary"], - "debug_args": kwargs["debug_args"], - "interactive": kwargs["interactive"]} + "debug_info": kwargs["debug_info"]} def executor_kwargs(test_type, server_config, cache_manager, **kwargs): @@ -39,17 +38,16 @@ def executor_kwargs(test_type, server_config, cache_manager, **kwargs): def env_options(): return {"host": "localhost", "bind_hostname": "true", - "testharnessreport": "testharnessreport-servo.js"} + "testharnessreport": "testharnessreport-servo.js", + "supports_debugger": True} class ServoBrowser(NullBrowser): - def __init__(self, logger, binary, debug_args=None, interactive=False): + def __init__(self, logger, binary, debug_info=None): NullBrowser.__init__(self, logger) self.binary = binary - self.debug_args = debug_args - self.interactive = interactive + self.debug_info = debug_info def executor_browser(self): return ExecutorBrowser, {"binary": self.binary, - "debug_args": self.debug_args, - "interactive": self.interactive} + "debug_info": self.debug_info} diff --git a/tests/wpt/harness/wptrunner/environment.py b/tests/wpt/harness/wptrunner/environment.py index b650dc5b11c..5d0436cbf5a 100644 --- a/tests/wpt/harness/wptrunner/environment.py +++ b/tests/wpt/harness/wptrunner/environment.py @@ -5,6 +5,7 @@ import json import os import multiprocessing +import signal import socket import sys import time @@ -18,6 +19,15 @@ here = os.path.split(__file__)[0] serve = None sslutils = None + +hostnames = ["web-platform.test", + "www.web-platform.test", + "www1.web-platform.test", + "www2.web-platform.test", + "xn--n8j6ds53lwwkrqhv28a.web-platform.test", + "xn--lve-6lad.web-platform.test"] + + def do_delayed_imports(logger, test_paths): global serve, sslutils @@ -90,7 +100,7 @@ class StaticHandler(object): class TestEnvironment(object): - def __init__(self, test_paths, ssl_env, pause_after_test, options): + def __init__(self, test_paths, ssl_env, pause_after_test, debug_info, options): """Context manager that owns the test environment i.e. the http and websockets servers""" self.test_paths = test_paths @@ -100,11 +110,13 @@ class TestEnvironment(object): self.external_config = None self.pause_after_test = pause_after_test self.test_server_port = options.pop("test_server_port", True) + self.debug_info = debug_info self.options = options if options is not None else {} self.cache_manager = multiprocessing.Manager() self.routes = self.get_routes() + def __enter__(self): self.ssl_env.__enter__() self.cache_manager.__enter__() @@ -113,9 +125,12 @@ class TestEnvironment(object): serve.set_computed_defaults(self.config) self.external_config, self.servers = serve.start(self.config, self.ssl_env, self.routes) + if self.options.get("supports_debugger") and self.debug_info and self.debug_info.interactive: + self.ignore_interrupts() return self def __exit__(self, exc_type, exc_val, exc_tb): + self.process_interrupts() self.cache_manager.__exit__(exc_type, exc_val, exc_tb) self.ssl_env.__exit__(exc_type, exc_val, exc_tb) @@ -123,6 +138,12 @@ class TestEnvironment(object): for port, server in servers: server.kill() + def ignore_interrupts(self): + signal.signal(signal.SIGINT, signal.SIG_IGN) + + def process_interrupts(self): + signal.signal(signal.SIGINT, signal.SIG_DFL) + def load_config(self): default_config_path = os.path.join(serve_path(self.test_paths), "config.default.json") local_config_path = os.path.join(here, "config.json") diff --git a/tests/wpt/harness/wptrunner/executors/base.py b/tests/wpt/harness/wptrunner/executors/base.py index 4fb9e87ec33..4aff10f0a77 100644 --- a/tests/wpt/harness/wptrunner/executors/base.py +++ b/tests/wpt/harness/wptrunner/executors/base.py @@ -21,7 +21,7 @@ def executor_kwargs(test_type, server_config, cache_manager, **kwargs): executor_kwargs = {"server_config": server_config, "timeout_multiplier": timeout_multiplier, - "debug_args": kwargs["debug_args"]} + "debug_info": kwargs["debug_info"]} if test_type == "reftest": executor_kwargs["screenshot_cache"] = cache_manager.dict() @@ -81,7 +81,7 @@ class TestExecutor(object): convert_result = None def __init__(self, browser, server_config, timeout_multiplier=1, - debug_args=None): + debug_info=None): """Abstract Base class for object that actually executes the tests in a specific browser. Typically there will be a different TestExecutor subclass for each test type and method of executing tests. @@ -97,8 +97,9 @@ class TestExecutor(object): self.browser = browser self.server_config = server_config self.timeout_multiplier = timeout_multiplier - self.debug_args = debug_args - self.last_protocol = "http" + self.debug_info = debug_info + self.last_environment = {"protocol": "http", + "prefs": []} self.protocol = None # This must be set in subclasses @property @@ -123,9 +124,8 @@ class TestExecutor(object): """Run a particular test. :param test: The test to run""" - - if test.protocol != self.last_protocol: - self.on_protocol_change(test.protocol) + if test.environment != self.last_environment: + self.on_environment_change(test.environment) try: result = self.do_test(test) @@ -138,7 +138,7 @@ class TestExecutor(object): if result[0].status == "ERROR": self.logger.debug(result[0].message) - self.last_protocol = test.protocol + self.last_environment = test.environment self.runner.send_message("test_ended", test, result) @@ -149,17 +149,17 @@ class TestExecutor(object): self.server_config["ports"][protocol][0]) def test_url(self, test): - return urlparse.urljoin(self.server_url(test.protocol), test.url) + return urlparse.urljoin(self.server_url(test.environment["protocol"]), test.url) @abstractmethod def do_test(self, test): - """Test-type and protocol specific implmentation of running a + """Test-type and protocol specific implementation of running a specific test. :param test: The test to run.""" pass - def on_protocol_change(self, new_protocol): + def on_environment_change(self, new_environment): pass def result_from_exception(self, test, e): @@ -182,10 +182,10 @@ class RefTestExecutor(TestExecutor): convert_result = reftest_result_converter def __init__(self, browser, server_config, timeout_multiplier=1, screenshot_cache=None, - debug_args=None): + debug_info=None): TestExecutor.__init__(self, browser, server_config, timeout_multiplier=timeout_multiplier, - debug_args=debug_args) + debug_info=debug_info) self.screenshot_cache = screenshot_cache diff --git a/tests/wpt/harness/wptrunner/executors/executormarionette.py b/tests/wpt/harness/wptrunner/executors/executormarionette.py index e5de4b9f0a2..aadb51c2776 100644 --- a/tests/wpt/harness/wptrunner/executors/executormarionette.py +++ b/tests/wpt/harness/wptrunner/executors/executormarionette.py @@ -62,7 +62,7 @@ class MarionetteProtocol(Protocol): while True: success = self.marionette.wait_for_port(60) #When running in a debugger wait indefinitely for firefox to start - if success or self.executor.debug_args is None: + if success or self.executor.debug_info is None: break session_started = False @@ -131,12 +131,80 @@ class MarionetteProtocol(Protocol): self.marionette.execute_async_script(""); except errors.ScriptTimeoutException: pass - except (socket.timeout, errors.InvalidResponseException, IOError): + except (socket.timeout, IOError): break except Exception as e: self.logger.error(traceback.format_exc(e)) break + def on_environment_change(self, old_environment, new_environment): + #Unset all the old prefs + for name, _ in old_environment.get("prefs", []): + value = self.executor.original_pref_values[name] + if value is None: + self.clear_user_pref(name) + else: + self.set_pref(name, value) + + for name, value in new_environment.get("prefs", []): + self.executor.original_pref_values[name] = self.get_pref(name) + self.set_pref(name, value) + + def set_pref(self, name, value): + self.logger.info("Setting pref %s (%s)" % (name, value)) + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + script = """ + let prefInterface = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + let pref = '%s'; + let value = '%s'; + let type = prefInterface.getPrefType(pref); + switch(type) { + case prefInterface.PREF_STRING: + prefInterface.setCharPref(pref, value); + break; + case prefInterface.PREF_BOOL: + prefInterface.setBoolPref(pref, value); + break; + case prefInterface.PREF_INT: + prefInterface.setIntPref(pref, value); + break; + } + """ % (name, value) + self.marionette.execute_script(script) + self.marionette.set_context(self.marionette.CONTEXT_CONTENT) + + def clear_user_pref(self, name): + self.logger.info("Clearing pref %s" % (name)) + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + script = """ + let prefInterface = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + let pref = '%s'; + prefInterface.clearUserPref(pref); + """ % name + self.marionette.execute_script(script) + self.marionette.set_context(self.marionette.CONTEXT_CONTENT) + + def get_pref(self, name): + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + self.marionette.execute_script(""" + let prefInterface = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + let pref = '%s'; + let type = prefInterface.getPrefType(pref); + switch(type) { + case prefInterface.PREF_STRING: + return prefInterface.getCharPref(pref); + case prefInterface.PREF_BOOL: + return prefInterface.getBoolPref(pref); + case prefInterface.PREF_INT: + return prefInterface.getIntPref(pref); + case prefInterface.PREF_INVALID: + return null; + } + """ % (name)) + self.marionette.set_context(self.marionette.CONTEXT_CONTENT) class MarionetteRun(object): def __init__(self, logger, func, marionette, url, timeout): @@ -159,7 +227,7 @@ class MarionetteRun(object): # make that possible. It also seems to time out immediately if the # timeout is set too high. This works at least. self.marionette.set_script_timeout(2**31 - 1) - except (IOError, errors.InvalidResponseException): + except IOError: self.logger.error("Lost marionette connection before starting test") return Stop @@ -185,7 +253,7 @@ class MarionetteRun(object): except errors.ScriptTimeoutException: self.logger.debug("Got a marionette timeout") self.result = False, ("EXTERNAL-TIMEOUT", None) - except (socket.timeout, errors.InvalidResponseException, IOError): + except (socket.timeout, IOError): # This can happen on a crash # Also, should check after the test if the firefox process is still running # and otherwise ignore any other result and set it to crash @@ -203,28 +271,33 @@ class MarionetteRun(object): class MarionetteTestharnessExecutor(TestharnessExecutor): def __init__(self, browser, server_config, timeout_multiplier=1, close_after_done=True, - debug_args=None): + debug_info=None): """Marionette-based executor for testharness.js tests""" TestharnessExecutor.__init__(self, browser, server_config, timeout_multiplier=timeout_multiplier, - debug_args=debug_args) + debug_info=debug_info) self.protocol = MarionetteProtocol(self, browser) self.script = open(os.path.join(here, "testharness_marionette.js")).read() self.close_after_done = close_after_done self.window_id = str(uuid.uuid4()) + self.original_pref_values = {} + if marionette is None: do_delayed_imports() def is_alive(self): return self.protocol.is_alive() - def on_protocol_change(self, new_protocol): - self.protocol.load_runner(new_protocol) + def on_environment_change(self, new_environment): + self.protocol.on_environment_change(self.last_environment, new_environment) + + if new_environment["protocol"] != self.last_environment["protocol"]: + self.protocol.load_runner(new_environment["protocol"]) def do_test(self, test): - timeout = (test.timeout * self.timeout_multiplier if self.debug_args is None + timeout = (test.timeout * self.timeout_multiplier if self.debug_info is None else None) success, data = MarionetteRun(self.logger, @@ -258,18 +331,19 @@ class MarionetteTestharnessExecutor(TestharnessExecutor): class MarionetteRefTestExecutor(RefTestExecutor): def __init__(self, browser, server_config, timeout_multiplier=1, - screenshot_cache=None, close_after_done=True, debug_args=None): + screenshot_cache=None, close_after_done=True, debug_info=None): """Marionette-based executor for reftests""" RefTestExecutor.__init__(self, browser, server_config, screenshot_cache=screenshot_cache, timeout_multiplier=timeout_multiplier, - debug_args=debug_args) + debug_info=debug_info) self.protocol = MarionetteProtocol(self, browser) self.implementation = RefTestImplementation(self) self.close_after_done = close_after_done self.has_window = False + self.original_pref_values = {} with open(os.path.join(here, "reftest.js")) as f: self.script = f.read() @@ -279,6 +353,9 @@ class MarionetteRefTestExecutor(RefTestExecutor): def is_alive(self): return self.protocol.is_alive() + def on_environment_change(self, new_environment): + self.protocol.on_environment_change(self.last_environment, new_environment) + def do_test(self, test): if self.close_after_done and self.has_window: self.protocol.marionette.close() @@ -296,7 +373,7 @@ class MarionetteRefTestExecutor(RefTestExecutor): return self.convert_result(test, result) def screenshot(self, test): - timeout = test.timeout if self.debug_args is None else None + timeout = self.timeout_multiplier * test.timeout if self.debug_info is None else None test_url = self.test_url(test) diff --git a/tests/wpt/harness/wptrunner/executors/executorselenium.py b/tests/wpt/harness/wptrunner/executors/executorselenium.py index 4f26e4e2724..6298893503b 100644 --- a/tests/wpt/harness/wptrunner/executors/executorselenium.py +++ b/tests/wpt/harness/wptrunner/executors/executorselenium.py @@ -165,11 +165,11 @@ class SeleniumRun(object): class SeleniumTestharnessExecutor(TestharnessExecutor): def __init__(self, browser, server_config, timeout_multiplier=1, - close_after_done=True, capabilities=None, debug_args=None): + close_after_done=True, capabilities=None, debug_info=None): """Selenium-based executor for testharness.js tests""" TestharnessExecutor.__init__(self, browser, server_config, timeout_multiplier=timeout_multiplier, - debug_args=debug_args) + debug_info=debug_info) self.protocol = SeleniumProtocol(self, browser, capabilities) with open(os.path.join(here, "testharness_webdriver.js")) as f: self.script = f.read() @@ -206,14 +206,14 @@ class SeleniumTestharnessExecutor(TestharnessExecutor): class SeleniumRefTestExecutor(RefTestExecutor): def __init__(self, browser, server_config, timeout_multiplier=1, screenshot_cache=None, close_after_done=True, - debug_args=None, capabilities=None): + debug_info=None, capabilities=None): """Selenium WebDriver-based executor for reftests""" RefTestExecutor.__init__(self, browser, server_config, screenshot_cache=screenshot_cache, timeout_multiplier=timeout_multiplier, - debug_args=debug_args) + debug_info=debug_info) self.protocol = SeleniumProtocol(self, browser, capabilities=capabilities) self.implementation = RefTestImplementation(self) diff --git a/tests/wpt/harness/wptrunner/executors/executorservo.py b/tests/wpt/harness/wptrunner/executors/executorservo.py index 37ba19f7a54..ef2bffdb2b3 100644 --- a/tests/wpt/harness/wptrunner/executors/executorservo.py +++ b/tests/wpt/harness/wptrunner/executors/executorservo.py @@ -21,6 +21,7 @@ from .base import (ExecutorException, testharness_result_converter, reftest_result_converter) from .process import ProcessTestExecutor +from ..browsers.base import browser_command hosts_text = """127.0.0.1 web-platform.test 127.0.0.1 www.web-platform.test @@ -39,11 +40,11 @@ def make_hosts_file(): class ServoTestharnessExecutor(ProcessTestExecutor): convert_result = testharness_result_converter - def __init__(self, browser, server_config, timeout_multiplier=1, debug_args=None, + def __init__(self, browser, server_config, timeout_multiplier=1, debug_info=None, pause_after_test=False): ProcessTestExecutor.__init__(self, browser, server_config, timeout_multiplier=timeout_multiplier, - debug_args=debug_args) + debug_info=debug_info) self.pause_after_test = pause_after_test self.result_data = None self.result_flag = None @@ -61,40 +62,48 @@ class ServoTestharnessExecutor(ProcessTestExecutor): self.result_data = None self.result_flag = threading.Event() - self.command = [self.binary, "--cpu", "--hard-fail", "-z", self.test_url(test)] + debug_args, command = browser_command(self.binary, ["--cpu", "--hard-fail", "-z", self.test_url(test)], + self.debug_info) + + self.command = command if self.pause_after_test: self.command.remove("-z") - if self.debug_args: - self.command = list(self.debug_args) + self.command + self.command = debug_args + self.command env = os.environ.copy() env["HOST_FILE"] = self.hosts_path - self.proc = ProcessHandler(self.command, - processOutputLine=[self.on_output], - onFinish=self.on_finish, - env=env) + + + if not self.interactive: + self.proc = ProcessHandler(self.command, + processOutputLine=[self.on_output], + onFinish=self.on_finish, + env=env, + storeOutput=False) + self.proc.run() + else: + self.proc = subprocess.Popen(self.command, env=env) try: - self.proc.run() - timeout = test.timeout * self.timeout_multiplier # Now wait to get the output we expect, or until we reach the timeout - if self.debug_args is None and not self.pause_after_test: + if not self.interactive and not self.pause_after_test: wait_timeout = timeout + 5 + self.result_flag.wait(wait_timeout) else: wait_timeout = None - self.result_flag.wait(wait_timeout) + self.proc.wait() proc_is_running = True if self.result_flag.is_set() and self.result_data is not None: self.result_data["test"] = test.url result = self.convert_result(test, self.result_data) else: - if self.proc.proc.poll() is not None: + if self.proc.poll() is not None: result = (test.result_cls("CRASH", None), []) proc_is_running = False else: @@ -150,13 +159,13 @@ class ServoRefTestExecutor(ProcessTestExecutor): convert_result = reftest_result_converter def __init__(self, browser, server_config, binary=None, timeout_multiplier=1, - screenshot_cache=None, debug_args=None, pause_after_test=False): + screenshot_cache=None, debug_info=None, pause_after_test=False): ProcessTestExecutor.__init__(self, browser, server_config, timeout_multiplier=timeout_multiplier, - debug_args=debug_args) + debug_info=debug_info) self.protocol = Protocol(self, browser) self.screenshot_cache = screenshot_cache diff --git a/tests/wpt/harness/wptrunner/executors/process.py b/tests/wpt/harness/wptrunner/executors/process.py index bff8b5bfb53..45f33ab2c75 100644 --- a/tests/wpt/harness/wptrunner/executors/process.py +++ b/tests/wpt/harness/wptrunner/executors/process.py @@ -9,7 +9,8 @@ class ProcessTestExecutor(TestExecutor): def __init__(self, *args, **kwargs): TestExecutor.__init__(self, *args, **kwargs) self.binary = self.browser.binary - self.interactive = self.browser.interactive + self.interactive = (False if self.debug_info is None + else self.debug_info.interactive) def setup(self, runner): self.runner = runner diff --git a/tests/wpt/harness/wptrunner/manifestexpected.py b/tests/wpt/harness/wptrunner/manifestexpected.py index b52e3bc14b3..beebdd35ab6 100644 --- a/tests/wpt/harness/wptrunner/manifestexpected.py +++ b/tests/wpt/harness/wptrunner/manifestexpected.py @@ -107,6 +107,15 @@ class TestNode(ManifestItem): except KeyError: return False + def prefs(self): + try: + prefs = self.get("prefs") + if type(prefs) in (str, unicode): + prefs = [prefs] + return [item.split(":", 1) for item in prefs] + except KeyError: + return [] + def append(self, node): """Add a subtest to the current test diff --git a/tests/wpt/harness/wptrunner/manifestinclude.py b/tests/wpt/harness/wptrunner/manifestinclude.py index 62d206cdccc..a3cda5a1c99 100644 --- a/tests/wpt/harness/wptrunner/manifestinclude.py +++ b/tests/wpt/harness/wptrunner/manifestinclude.py @@ -9,6 +9,7 @@ representing the file and each subnode representing a subdirectory that should be included or excluded. """ import os +import urlparse from wptmanifest.node import DataNode from wptmanifest.backends import conditional @@ -42,7 +43,7 @@ class IncludeManifest(ManifestItem): this object. :param test: The test object""" - path_components = self._get_path_components(test) + path_components = self._get_components(test.url) return self._include(test, path_components) def _include(self, test, path_components): @@ -64,21 +65,41 @@ class IncludeManifest(ManifestItem): # Include by default return True - def _get_path_components(self, test): - test_url = test.url - assert test_url[0] == "/" - return [item for item in reversed(test_url.split("/")) if item] + def _get_components(self, url): + rv = [] + url_parts = urlparse.urlsplit(url) + variant = "" + if url_parts.query: + variant += "?" + url_parts.query + if url_parts.fragment: + variant += "#" + url_parts.fragment + if variant: + rv.append(variant) + rv.extend([item for item in reversed(url_parts.path.split("/")) if item]) + return rv def _add_rule(self, test_manifests, url, direction): - maybe_path = os.path.abspath(os.path.join(os.curdir, url)) + maybe_path = os.path.join(os.path.abspath(os.curdir), url) + rest, last = os.path.split(maybe_path) + variant = "" + if "#" in last: + last, fragment = last.rsplit("#", 1) + variant += "#" + fragment + if "?" in last: + last, query = last.rsplit("?", 1) + variant += "?" + query + + maybe_path = os.path.join(rest, last) + if os.path.exists(maybe_path): for manifest, data in test_manifests.iteritems(): rel_path = os.path.relpath(maybe_path, data["tests_path"]) if ".." not in rel_path.split(os.sep): - url = rel_path + url = data["url_base"] + rel_path.replace(os.path.sep, "/") + variant + break assert direction in ("include", "exclude") - components = [item for item in reversed(url.split("/")) if item] + components = self._get_components(url) node = self while components: diff --git a/tests/wpt/harness/wptrunner/metadata.py b/tests/wpt/harness/wptrunner/metadata.py index f8210227335..3a35619c5e7 100644 --- a/tests/wpt/harness/wptrunner/metadata.py +++ b/tests/wpt/harness/wptrunner/metadata.py @@ -186,7 +186,7 @@ def write_new_expected(metadata_path, expected_map): if not os.path.exists(dir): os.makedirs(dir) with open(path, "w") as f: - f.write(manifest_str.encode("utf8")) + f.write(manifest_str) class ExpectedUpdater(object): diff --git a/tests/wpt/harness/wptrunner/testloader.py b/tests/wpt/harness/wptrunner/testloader.py index bec351a5cfd..9270bec2cc1 100644 --- a/tests/wpt/harness/wptrunner/testloader.py +++ b/tests/wpt/harness/wptrunner/testloader.py @@ -190,8 +190,6 @@ class EqualTimeChunker(TestChunker): class TestFilter(object): def __init__(self, test_manifests, include=None, exclude=None, manifest_path=None): - test_manifests = test_manifests - if manifest_path is not None and include is None: self.manifest = manifestinclude.get_manifest(manifest_path) else: @@ -355,7 +353,7 @@ class TestLoader(object): for test_path, test_type, test in self.iter_tests(): enabled = not test.disabled() - if not self.include_https and test.protocol == "https": + if not self.include_https and test.environment["protocol"] == "https": enabled = False key = "enabled" if enabled else "disabled" tests[key][test_type].append(test) diff --git a/tests/wpt/harness/wptrunner/testrunner.py b/tests/wpt/harness/wptrunner/testrunner.py index 95095906b38..063fa7d159e 100644 --- a/tests/wpt/harness/wptrunner/testrunner.py +++ b/tests/wpt/harness/wptrunner/testrunner.py @@ -168,7 +168,7 @@ class TestRunnerManager(threading.Thread): def __init__(self, suite_name, test_queue, test_source_cls, browser_cls, browser_kwargs, executor_cls, executor_kwargs, stop_flag, pause_after_test=False, - pause_on_unexpected=False, debug_args=None): + pause_on_unexpected=False, debug_info=None): """Thread that owns a single TestRunner process and any processes required by the TestRunner (e.g. the Firefox binary). @@ -206,7 +206,7 @@ class TestRunnerManager(threading.Thread): self.pause_after_test = pause_after_test self.pause_on_unexpected = pause_on_unexpected - self.debug_args = debug_args + self.debug_info = debug_info self.manager_number = next_manager_number() @@ -333,7 +333,7 @@ class TestRunnerManager(threading.Thread): with self.init_lock: # Guard against problems initialising the browser or the browser # remote control method - if self.debug_args is None: + if self.debug_info is None: self.init_timer = threading.Timer(self.browser.init_timeout, init_failed) test_queue = self.test_source.get_queue() @@ -560,7 +560,6 @@ class TestQueue(object): self.test_type = test_type self.tests = tests self.kwargs = kwargs - self.queue = None def __enter__(self): if not self.tests[self.test_type]: @@ -590,7 +589,7 @@ class ManagerGroup(object): executor_cls, executor_kwargs, pause_after_test=False, pause_on_unexpected=False, - debug_args=None): + debug_info=None): """Main thread object that owns all the TestManager threads.""" self.suite_name = suite_name self.size = size @@ -602,7 +601,7 @@ class ManagerGroup(object): self.executor_kwargs = executor_kwargs self.pause_after_test = pause_after_test self.pause_on_unexpected = pause_on_unexpected - self.debug_args = debug_args + self.debug_info = debug_info self.pool = set() # Event that is polled by threads so that they can gracefully exit in the face @@ -640,7 +639,7 @@ class ManagerGroup(object): self.stop_flag, self.pause_after_test, self.pause_on_unexpected, - self.debug_args) + self.debug_info) manager.start() self.pool.add(manager) self.wait() diff --git a/tests/wpt/harness/wptrunner/update/sync.py b/tests/wpt/harness/wptrunner/update/sync.py index 6a58696d545..68db2d3285b 100644 --- a/tests/wpt/harness/wptrunner/update/sync.py +++ b/tests/wpt/harness/wptrunner/update/sync.py @@ -4,6 +4,7 @@ import os import shutil +import sys import uuid from .. import testloader @@ -93,6 +94,14 @@ class UpdateCheckout(Step): sync_tree.update(state.sync["remote_url"], state.sync["branch"], state.local_branch) + sync_path = os.path.abspath(sync_tree.root) + if not sync_path in sys.path: + from update import setup_paths + setup_paths(sync_path) + + def restore(self, state): + assert os.path.abspath(state.sync_tree.root) in sys.path + Step.restore(self, state) class GetSyncTargetCommit(Step): diff --git a/tests/wpt/harness/wptrunner/update/update.py b/tests/wpt/harness/wptrunner/update/update.py index 3ef4b3f1c07..2fb9443349e 100644 --- a/tests/wpt/harness/wptrunner/update/update.py +++ b/tests/wpt/harness/wptrunner/update/update.py @@ -14,7 +14,7 @@ from base import Step, StepRunner, exit_clean, exit_unclean from state import State def setup_paths(sync_path): - sys.path.insert(0, sync_path) + sys.path.insert(0, os.path.abspath(sync_path)) from tools import localpaths class LoadConfig(Step): @@ -117,7 +117,9 @@ class WPTUpdate(object): if not kwargs["sync"]: setup_paths(self.serve_root) else: - setup_paths(kwargs["sync_path"]) + if os.path.exists(kwargs["sync_path"]): + # If the sync path doesn't exist we defer this until it does + setup_paths(kwargs["sync_path"]) self.state = State(logger) self.kwargs = kwargs diff --git a/tests/wpt/harness/wptrunner/wptcommandline.py b/tests/wpt/harness/wptrunner/wptcommandline.py index 1622f87db99..bf00901ab37 100644 --- a/tests/wpt/harness/wptrunner/wptcommandline.py +++ b/tests/wpt/harness/wptrunner/wptcommandline.py @@ -25,12 +25,6 @@ def url_or_path(path): else: return abs_path(path) -def slash_prefixed(url): - if not url.startswith("/"): - url = "/" + url - return url - - def require_arg(kwargs, name, value_func=None): if value_func is None: value_func = lambda x: x is not None @@ -97,15 +91,15 @@ def create_parser(product_choices=None): nargs="*", default=["testharness", "reftest"], choices=["testharness", "reftest"], help="Test types to run") - test_selection_group.add_argument("--include", action="append", type=slash_prefixed, + test_selection_group.add_argument("--include", action="append", help="URL prefix to include") - test_selection_group.add_argument("--exclude", action="append", type=slash_prefixed, + test_selection_group.add_argument("--exclude", action="append", help="URL prefix to exclude") test_selection_group.add_argument("--include-manifest", type=abs_path, help="Path to manifest listing tests to include") debugging_group = parser.add_argument_group("Debugging") - debugging_group.add_argument('--debugger', + debugging_group.add_argument('--debugger', const="__default__", nargs="?", help="run under a debugger, e.g. gdb or valgrind") debugging_group.add_argument('--debugger-args', help="arguments to the debugger") @@ -233,8 +227,6 @@ def exe_path(name): def check_args(kwargs): - from mozrunner import debugger_arguments - set_from_config(kwargs) for test_paths in kwargs["test_paths"].itervalues(): @@ -278,16 +270,18 @@ def check_args(kwargs): kwargs["processes"] = 1 if kwargs["debugger"] is not None: - debug_args, interactive = debugger_arguments(kwargs["debugger"], - kwargs["debugger_args"]) - if interactive: - require_arg(kwargs, "processes", lambda x: x == 1) + import mozdebug + if kwargs["debugger"] == "__default__": + kwargs["debugger"] = mozdebug.get_default_debugger_name() + debug_info = mozdebug.get_debugger_info(kwargs["debugger"], + kwargs["debugger_args"]) + if debug_info.interactive: + if kwargs["processes"] != 1: + kwargs["processes"] = 1 kwargs["no_capture_stdio"] = True - kwargs["interactive"] = interactive - kwargs["debug_args"] = debug_args + kwargs["debug_info"] = debug_info else: - kwargs["interactive"] = False - kwargs["debug_args"] = None + kwargs["debug_info"] = None if kwargs["binary"] is not None: if not os.path.exists(kwargs["binary"]): diff --git a/tests/wpt/harness/wptrunner/wptmanifest/backends/conditional.py b/tests/wpt/harness/wptrunner/wptmanifest/backends/conditional.py index 96d2b0ca54c..eed7c8aabac 100644 --- a/tests/wpt/harness/wptrunner/wptmanifest/backends/conditional.py +++ b/tests/wpt/harness/wptrunner/wptmanifest/backends/conditional.py @@ -4,7 +4,7 @@ import operator -from ..node import NodeVisitor, DataNode, ConditionalNode, KeyValueNode, ValueNode +from ..node import NodeVisitor, DataNode, ConditionalNode, KeyValueNode, ListNode, ValueNode from ..parser import parse @@ -17,13 +17,16 @@ class ConditionalValue(object): self.condition_node = self.node.children[0] self.value_node = self.node.children[1] else: - assert isinstance(node, ValueNode) + assert isinstance(node, (ValueNode, ListNode)) self.condition_node = None self.value_node = self.node @property def value(self): - return self.value_node.data + if isinstance(self.value_node, ValueNode): + return self.value_node.data + else: + return [item.data for item in self.value_node.children] @value.setter def value(self, value): @@ -106,6 +109,9 @@ class Compiler(NodeVisitor): self.output_node._add_key_value(node, key_values) + def visit_ListNode(self, node): + return (lambda x:True, [self.visit(child) for child in node.children]) + def visit_ValueNode(self, node): return (lambda x: True, node.data) diff --git a/tests/wpt/harness/wptrunner/wptmanifest/backends/static.py b/tests/wpt/harness/wptrunner/wptmanifest/backends/static.py index 425473719ac..7120e89de04 100644 --- a/tests/wpt/harness/wptrunner/wptmanifest/backends/static.py +++ b/tests/wpt/harness/wptrunner/wptmanifest/backends/static.py @@ -68,6 +68,9 @@ class Compiler(NodeVisitor): def visit_ValueNode(self, node): return node.data + def visit_ListNode(self, node): + return [self.visit(child) for child in node.children] + def visit_ConditionalNode(self, node): assert len(node.children) == 2 if self.visit(node.children[0]): diff --git a/tests/wpt/harness/wptrunner/wptmanifest/node.py b/tests/wpt/harness/wptrunner/wptmanifest/node.py index 37762f539e6..bb40535936e 100644 --- a/tests/wpt/harness/wptrunner/wptmanifest/node.py +++ b/tests/wpt/harness/wptrunner/wptmanifest/node.py @@ -82,6 +82,12 @@ class KeyValueNode(Node): self.children.append(other) +class ListNode(Node): + def append(self, other): + other.parent = self + self.children.append(other) + + class ValueNode(Node): def append(self, other): raise TypeError diff --git a/tests/wpt/harness/wptrunner/wptmanifest/parser.py b/tests/wpt/harness/wptrunner/wptmanifest/parser.py index 1b121f04847..28f9666ac55 100644 --- a/tests/wpt/harness/wptrunner/wptmanifest/parser.py +++ b/tests/wpt/harness/wptrunner/wptmanifest/parser.py @@ -23,7 +23,12 @@ from node import * class ParseError(Exception): - pass + def __init__(self, filename, line, detail): + self.line = line + self.filename = filename + self.detail = detail + self.message = "%s: %s line %s" % (self.detail, self.filename, self.line) + Exception.__init__(self, self.message) eol = object group_start = object @@ -41,7 +46,7 @@ operators = ["==", "!=", "not", "and", "or"] def decode(byte_str): - return byte_str.decode("string_escape").decode("utf8") + return byte_str.decode("utf8") def precedence(operator_node): @@ -50,7 +55,7 @@ def precedence(operator_node): class TokenTypes(object): def __init__(self): - for type in ["group_start", "group_end", "paren", "separator", "ident", "string", "number", "eof"]: + for type in ["group_start", "group_end", "paren", "list_start", "list_end", "separator", "ident", "string", "number", "eof"]: setattr(self, type, type) token_types = TokenTypes() @@ -70,18 +75,27 @@ class Tokenizer(object): self.reset() if type(stream) in types.StringTypes: stream = StringIO(stream) + if not hasattr(stream, "name"): + self.filename = "" + else: + self.filename = stream.name + self.next_line_state = self.line_start_state for i, line in enumerate(stream): - self.state = self.line_start_state + self.state = self.next_line_state + assert self.state is not None + states = [] + self.next_line_state = None self.line_number = i + 1 self.index = 0 self.line = line.rstrip() - if self.line: - while self.state != self.eol_state: - tokens = self.state() - if tokens: - for token in tokens: - yield token + while self.state != self.eol_state: + states.append(self.state) + tokens = self.state() + if tokens: + for token in tokens: + yield token + self.state() while True: yield (token_types.eof, None) @@ -102,11 +116,14 @@ class Tokenizer(object): self.consume() def eol_state(self): - pass + if self.next_line_state is None: + self.next_line_state = self.line_start_state def line_start_state(self): self.skip_whitespace() - assert self.char() != eol + if self.char() == eol: + self.state = self.eol_state + return if self.index > self.indent_levels[-1]: self.indent_levels.append(self.index) yield (token_types.group_start, None) @@ -119,7 +136,7 @@ class Tokenizer(object): # it must always be a heading or key next so we go back to data_line_state self.next_state = self.data_line_state if self.index != self.indent_levels[-1]: - raise ParseError("Unexpected indent") + raise ParseError(self.filename, self.line_number, "Unexpected indent") self.state = self.next_state @@ -132,57 +149,44 @@ class Tokenizer(object): self.state = self.key_state def heading_state(self): - index_0 = self.index - skip_indexes = [] + rv = "" while True: c = self.char() if c == "\\": - self.consume() - c = self.char() - if c == eol: - raise ParseError("Unexpected EOL in heading") - elif c == "]": - skip_indexes.append(self.index - 1) - self.consume() + rv += self.consume_escape() elif c == "]": break elif c == eol: - raise ParseError("EOL in heading") + raise ParseError(self.filename, self.line_number, "EOL in heading") else: + rv += c self.consume() - self.state = self.line_end_state - index_1 = self.index - parts = [] - min_index = index_0 - for index in skip_indexes: - parts.append(self.line[min_index:index]) - min_index = index + 1 - parts.append(self.line[min_index:index_1]) - yield (token_types.string, decode("".join(parts))) + yield (token_types.string, decode(rv)) yield (token_types.paren, "]") self.consume() self.state = self.line_end_state self.next_state = self.data_line_state def key_state(self): - index_0 = self.index + rv = "" while True: c = self.char() if c == " ": - index_1 = self.index self.skip_whitespace() if self.char() != ":": - raise ParseError("Space in key name") + raise ParseError(self.filename, self.line_number, "Space in key name") break elif c == ":": - index_1 = self.index break elif c == eol: - raise ParseError("EOL in key name (missing ':'?)") + raise ParseError(self.filename, self.line_number, "EOL in key name (missing ':'?)") + elif c == "\\": + rv += self.consume_escape() else: + rv += c self.consume() - yield (token_types.string, decode(self.line[index_0:index_1])) + yield (token_types.string, decode(rv)) yield (token_types.separator, ":") self.consume() self.state = self.after_key_state @@ -196,39 +200,115 @@ class Tokenizer(object): elif c == eol: self.next_state = self.expr_or_value_state self.state = self.eol_state + elif c == "[": + self.state = self.list_start_state else: self.state = self.value_state + def list_start_state(self): + yield (token_types.list_start, "[") + self.consume() + self.state = self.list_value_start_state + + def list_value_start_state(self): + self.skip_whitespace() + if self.char() == "]": + self.state = self.list_end_state + elif self.char() in ("'", '"'): + quote_char = self.char() + self.consume() + yield (token_types.string, self.consume_string(quote_char)) + self.skip_whitespace() + if self.char() == "]": + self.state = self.list_end_state + elif self.char() != ",": + raise ParseError(self.filename, self.line_number, "Junk after quoted string") + self.consume() + elif self.char() == "#": + self.state = self.comment_state + self.next_line_state = self.list_value_start_state + elif self.char() == eol: + self.next_line_state = self.list_value_start_state + self.state = self.eol_state + elif self.char() == ",": + raise ParseError(self.filename, self.line_number, "List item started with separator") + else: + self.state = self.list_value_state + + def list_value_state(self): + rv = "" + spaces = 0 + while True: + c = self.char() + if c == "\\": + escape = self.consume_escape() + rv += escape + elif c == eol: + raise ParseError(self.filename, self.line_number, "EOL in list value") + elif c == "#": + raise ParseError(self.filename, self.line_number, "EOL in list value (comment)") + elif c == ",": + self.state = self.list_value_start_state + self.consume() + break + elif c == " ": + spaces += 1 + self.consume() + elif c == "]": + self.state = self.list_end_state + self.consume() + break + else: + rv += " " * spaces + spaces = 0 + rv += c + self.consume() + + if rv: + yield (token_types.string, decode(rv)) + + def list_end_state(self): + self.consume() + yield (token_types.list_end, "]") + self.state = self.line_end_state + def value_state(self): self.skip_whitespace() - index_0 = self.index if self.char() in ("'", '"'): quote_char = self.char() self.consume() - yield (token_types.string, decode(self.read_string(quote_char))) + yield (token_types.string, self.consume_string(quote_char)) + if self.char() == "#": + self.state = self.comment_state + else: + self.state = self.line_end_state else: - index_1 = self.index + rv = "" + spaces = 0 while True: c = self.char() if c == "\\": - self.consume() - if self.char() == eol: - raise ParseError("EOL in character escape") + rv += self.consume_escape() elif c == "#": self.state = self.comment_state break elif c == " ": # prevent whitespace before comments from being included in the value - pass + spaces += 1 + self.consume() elif c == eol: + self.state = self.line_end_state break else: - index_1 = self.index - self.consume() - yield (token_types.string, decode(self.line[index_0:index_1 + 1])) - self.state = self.line_end_state + rv += " " * spaces + spaces = 0 + rv += c + self.consume() + yield (token_types.string, decode(rv)) def comment_state(self): + while self.char() is not eol: + self.consume() self.state = self.eol_state def line_end_state(self): @@ -239,26 +319,24 @@ class Tokenizer(object): elif c == eol: self.state = self.eol_state else: - raise ParseError("Junk before EOL c") + raise ParseError(self.filename, self.line_number, "Junk before EOL %s" % c) - def read_string(self, quote_char): - index_0 = self.index + def consume_string(self, quote_char): + rv = "" while True: c = self.char() if c == "\\": - self.consume() - if self.char == eol: - raise ParseError("EOL following quote") - self.consume() + rv += self.consume_escape() elif c == quote_char: + self.consume() break elif c == eol: - raise ParseError("EOL in quoted string") + raise ParseError(self.filename, self.line_number, "EOL in quoted string") else: + rv += c self.consume() - rv = self.line[index_0:self.index] - self.consume() - return rv + + return decode(rv) def expr_or_value_state(self): if self.peek(3) == "if ": @@ -270,12 +348,12 @@ class Tokenizer(object): self.skip_whitespace() c = self.char() if c == eol: - raise ParseError("EOL in expression") + raise ParseError(self.filename, self.line_number, "EOL in expression") elif c in "'\"": self.consume() - yield (token_types.string, decode(self.read_string(c))) + yield (token_types.string, self.consume_string(c)) elif c == "#": - raise ParseError("Comment before end of expression") + raise ParseError(self.filename, self.line_number, "Comment before end of expression") elif c == ":": yield (token_types.separator, c) self.consume() @@ -315,7 +393,7 @@ class Tokenizer(object): self.consume() elif c == ".": if seen_dot: - raise ParseError("Invalid number") + raise ParseError(self.filename, self.line_number, "Invalid number") self.consume() seen_dot = True elif c in parens: @@ -327,7 +405,7 @@ class Tokenizer(object): elif c == ":": break else: - raise ParseError("Invalid character in number") + raise ParseError(self.filename, self.line_number, "Invalid character in number") self.state = self.expr_state yield (token_types.number, self.line[index_0:self.index]) @@ -353,6 +431,44 @@ class Tokenizer(object): self.state = self.expr_state yield (token_types.ident, self.line[index_0:self.index]) + def consume_escape(self): + assert self.char() == "\\" + self.consume() + c = self.char() + self.consume() + if c == "x": + return self.decode_escape(2) + elif c == "u": + return self.decode_escape(4) + elif c == "U": + return self.decode_escape(6) + elif c in ["a", "b", "f", "n", "r", "t", "v"]: + return eval("'\%s'" % c) + elif c is eol: + raise ParseError(self.filename, self.line_number, "EOL in escape") + else: + return c + + def decode_escape(self, length): + value = 0 + for i in xrange(length): + c = self.char() + value *= 16 + value += self.escape_value(c) + self.consume() + + return unichr(value).encode("utf8") + + def escape_value(self, c): + if '0' <= c <= '9': + return ord(c) - ord('0') + elif 'a' <= c <= 'f': + return ord(c) - ord('a') + 10 + elif 'A' <= c <= 'F': + return ord(c) - ord('A') + 10 + else: + raise ParseError(self.filename, self.line_number, "Invalid character escape") + class Parser(object): def __init__(self): @@ -417,7 +533,10 @@ class Parser(object): self.expect(token_types.group_end) def value_block(self): - if self.token[0] == token_types.string: + if self.token[0] == token_types.list_start: + self.consume() + self.list_value() + elif self.token[0] == token_types.string: self.value() elif self.token[0] == token_types.group_start: self.consume() @@ -428,6 +547,13 @@ class Parser(object): else: raise ParseError + def list_value(self): + self.tree.append(ListNode()) + while self.token[0] == token_types.string: + self.value() + self.expect(token_types.list_end) + self.tree.pop() + def expression_values(self): while self.token == (token_types.ident, "if"): self.consume() @@ -446,7 +572,7 @@ class Parser(object): self.tree.pop() def expr_start(self): - self.expr_builder = ExpressionBuilder() + self.expr_builder = ExpressionBuilder(self.tokenizer) self.expr_builders.append(self.expr_builder) self.expr() expression = self.expr_builder.finish() @@ -486,14 +612,14 @@ class Parser(object): self.expr_builder.push_operator(UnaryOperatorNode(self.token[1])) self.consume() else: - raise ParseError() + raise ParseError(self.filename, self.tokenizer.line_number, "Expected unary operator") def expr_bin_op(self): if self.token[1] in binary_operators: self.expr_builder.push_operator(BinaryOperatorNode(self.token[1])) self.consume() else: - raise ParseError() + raise ParseError(self.filename, self.tokenizer.line_number, "Expected binary operator") def expr_value(self): node_type = {token_types.string: StringNode, @@ -528,9 +654,10 @@ class Treebuilder(object): class ExpressionBuilder(object): - def __init__(self): + def __init__(self, tokenizer): self.operands = [] self.operators = [None] + self.tokenizer = tokenizer def finish(self): while self.operators[-1] is not None: @@ -546,7 +673,8 @@ class ExpressionBuilder(object): while self.operators[-1] is not None: self.pop_operator() if not self.operators: - raise ParseError("Unbalanced parens") + raise ParseError(self.tokenizer.filename, self.tokenizer.line, + "Unbalanced parens") assert self.operators.pop() is None diff --git a/tests/wpt/harness/wptrunner/wptmanifest/serializer.py b/tests/wpt/harness/wptrunner/wptmanifest/serializer.py index 9a5ef2fd9fa..d87ca0f3be9 100644 --- a/tests/wpt/harness/wptrunner/wptmanifest/serializer.py +++ b/tests/wpt/harness/wptrunner/wptmanifest/serializer.py @@ -2,15 +2,25 @@ # 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/. -from node import NodeVisitor, ValueNode, BinaryExpressionNode +from node import NodeVisitor, ValueNode, ListNode, BinaryExpressionNode from parser import precedence +named_escapes = set(["\a", "\b", "\f", "\n", "\r", "\t", "\v"]) def escape(string, extras=""): - rv = string.encode("utf8").encode("string_escape") - for extra in extras: - rv = rv.replace(extra, "\\" + extra) - return rv + rv = "" + for c in string: + if c in named_escapes: + rv += c.encode("unicode_escape") + elif c == "\\": + rv += "\\\\" + elif c < '\x20': + rv += "\\x%02x" % ord(c) + elif c in extras: + rv += "\\" + c + else: + rv += c + return rv.encode("utf8") class ManifestSerializer(NodeVisitor): @@ -42,34 +52,48 @@ class ManifestSerializer(NodeVisitor): return rv def visit_KeyValueNode(self, node): - rv = [node.data + ":"] + rv = [escape(node.data, ":") + ":"] indent = " " * self.indent - if len(node.children) == 1 and isinstance(node.children[0], ValueNode): - rv[0] += " %s" % escape(self.visit(node.children[0])[0]) + if len(node.children) == 1 and isinstance(node.children[0], (ValueNode, ListNode)): + rv[0] += " %s" % self.visit(node.children[0])[0] else: for child in node.children: rv.append(indent + self.visit(child)[0]) return rv + def visit_ListNode(self, node): + rv = ["["] + rv.extend(", ".join(self.visit(child)[0] for child in node.children)) + rv.append("]") + return ["".join(rv)] + def visit_ValueNode(self, node): - return [escape(node.data)] + if "#" in node.data or (isinstance(node.parent, ListNode) and + ("," in node.data or "]" in node.data)): + if "\"" in node.data: + quote = "'" + else: + quote = "\"" + else: + quote = "" + return [quote + escape(node.data, extras=quote) + quote] def visit_ConditionalNode(self, node): return ["if %s: %s" % tuple(self.visit(item)[0] for item in node.children)] def visit_StringNode(self, node): - rv = ["\"%s\"" % node.data] + rv = ["\"%s\"" % escape(node.data, extras="\"")] for child in node.children: rv[0] += self.visit(child)[0] return rv def visit_NumberNode(self, node): - return [node.data] + return [str(node.data)] def visit_VariableNode(self, node): - rv = node.data + rv = escape(node.data) for child in node.children: rv += self.visit(child) return [rv] @@ -100,10 +124,10 @@ class ManifestSerializer(NodeVisitor): return [" ".join(children)] def visit_UnaryOperatorNode(self, node): - return [node.data] + return [str(node.data)] def visit_BinaryOperatorNode(self, node): - return [node.data] + return [str(node.data)] def serialize(tree, *args, **kwargs): diff --git a/tests/wpt/harness/wptrunner/wptmanifest/tests/test_serializer.py b/tests/wpt/harness/wptrunner/wptmanifest/tests/test_serializer.py index 0ba3209ef84..236b56cc7fc 100644 --- a/tests/wpt/harness/wptrunner/wptmanifest/tests/test_serializer.py +++ b/tests/wpt/harness/wptrunner/wptmanifest/tests/test_serializer.py @@ -15,12 +15,12 @@ class TokenizerTest(unittest.TestCase): self.parser = parser.Parser() def serialize(self, input_str): - return self.serializer.serialize(self.parser.parse(StringIO(input_str))) + return self.serializer.serialize(self.parser.parse(input_str)) def compare(self, input_str, expected=None): if expected is None: expected = input_str - + expected = expected.encode("utf8") actual = self.serialize(input_str) self.assertEquals(actual, expected) @@ -114,11 +114,98 @@ class TokenizerTest(unittest.TestCase): [Heading 2] other_key: other_value -""" - ) +""") def test_11(self): self.compare("""key: if not a and b and c and d: true -""" - ) +""") + + def test_12(self): + self.compare("""[Heading 1] + key: [a:1, b:2] +""") + + def test_13(self): + self.compare("""key: [a:1, "b:#"] +""") + + def test_14(self): + self.compare("""key: [","] +""") + + def test_15(self): + self.compare("""key: , +""") + + def test_16(self): + self.compare("""key: ["]", b] +""") + + def test_17(self): + self.compare("""key: ] +""") + + def test_18(self): + self.compare("""key: \] + """, """key: ] +""") + + def test_escape_0(self): + self.compare(r"""k\t\:y: \a\b\f\n\r\t\v""", + r"""k\t\:y: \x07\x08\x0c\n\r\t\x0b +""") + + def test_escape_1(self): + self.compare(r"""k\x00: \x12A\x45""", + r"""k\x00: \x12AE +""") + + def test_escape_2(self): + self.compare(r"""k\u0045y: \u1234A\uABc6""", + u"""kEy: \u1234A\uabc6 +""") + + def test_escape_3(self): + self.compare(r"""k\u0045y: \u1234A\uABc6""", + u"""kEy: \u1234A\uabc6 +""") + + def test_escape_4(self): + self.compare(r"""key: '\u1234A\uABc6'""", + u"""key: \u1234A\uabc6 +""") + + def test_escape_5(self): + self.compare(r"""key: [\u1234A\uABc6]""", + u"""key: [\u1234A\uabc6] +""") + + def test_escape_6(self): + self.compare(r"""key: [\u1234A\uABc6\,]""", + u"""key: ["\u1234A\uabc6,"] +""") + + def test_escape_7(self): + self.compare(r"""key: [\,\]\#]""", + r"""key: [",]#"] +""") + + def test_escape_8(self): + self.compare(r"""key: \#""", + r"""key: "#" +""") + + def test_escape_9(self): + self.compare(r"""key: \U10FFFFabc""", + u"""key: \U0010FFFFabc +""") + + def test_escape_10(self): + self.compare(r"""key: \u10FFab""", + u"""key: \u10FFab +""") + + def test_escape_11(self): + self.compare(r"""key: \\ab +""") diff --git a/tests/wpt/harness/wptrunner/wptmanifest/tests/test_tokenizer.py b/tests/wpt/harness/wptrunner/wptmanifest/tests/test_tokenizer.py index 3243a219c19..9395677135c 100644 --- a/tests/wpt/harness/wptrunner/wptmanifest/tests/test_tokenizer.py +++ b/tests/wpt/harness/wptrunner/wptmanifest/tests/test_tokenizer.py @@ -65,7 +65,7 @@ class TokenizerTest(unittest.TestCase): (token_types.paren, "]")]) def test_heading_6(self): - self.compare("""[Heading \\ttext]""", + self.compare(r"""[Heading \ttext]""", [(token_types.paren, "["), (token_types.string, "Heading \ttext"), (token_types.paren, "]")]) @@ -142,6 +142,76 @@ class TokenizerTest(unittest.TestCase): with self.assertRaises(parser.ParseError): self.tokenize("""key: 'value' abc""") + def test_key_14(self): + self.compare(r"""key: \\nb""", + [(token_types.string, "key"), + (token_types.separator, ":"), + (token_types.string, r"\nb")]) + + def test_list_0(self): + self.compare( +""" +key: []""", + [(token_types.string, "key"), + (token_types.separator, ":"), + (token_types.list_start, "["), + (token_types.list_end, "]")]) + + def test_list_1(self): + self.compare( +""" +key: [a, "b"]""", + [(token_types.string, "key"), + (token_types.separator, ":"), + (token_types.list_start, "["), + (token_types.string, "a"), + (token_types.string, "b"), + (token_types.list_end, "]")]) + + def test_list_2(self): + self.compare( +""" +key: [a, + b]""", + [(token_types.string, "key"), + (token_types.separator, ":"), + (token_types.list_start, "["), + (token_types.string, "a"), + (token_types.string, "b"), + (token_types.list_end, "]")]) + + def test_list_3(self): + self.compare( +""" +key: [a, #b] + c]""", + [(token_types.string, "key"), + (token_types.separator, ":"), + (token_types.list_start, "["), + (token_types.string, "a"), + (token_types.string, "c"), + (token_types.list_end, "]")]) + + def test_list_4(self): + with self.assertRaises(parser.ParseError): + self.tokenize("""key: [a #b] + c]""") + + def test_list_5(self): + with self.assertRaises(parser.ParseError): + self.tokenize("""key: [a \\ + c]""") + + def test_list_6(self): + self.compare( +"""key: [a , b]""", + [(token_types.string, "key"), + (token_types.separator, ":"), + (token_types.list_start, "["), + (token_types.string, "a"), + (token_types.string, "b"), + (token_types.list_end, "]")]) + def test_expr_0(self): self.compare( """ diff --git a/tests/wpt/harness/wptrunner/wptrunner.py b/tests/wpt/harness/wptrunner/wptrunner.py index cef210ee0b3..075f07cdc0d 100644 --- a/tests/wpt/harness/wptrunner/wptrunner.py +++ b/tests/wpt/harness/wptrunner/wptrunner.py @@ -134,6 +134,7 @@ def run_tests(config, test_paths, product, **kwargs): with env.TestEnvironment(test_paths, ssl_env, kwargs["pause_after_test"], + kwargs["debug_info"], env_options) as test_environment: try: test_environment.ensure_started() @@ -180,7 +181,7 @@ def run_tests(config, test_paths, product, **kwargs): executor_kwargs, kwargs["pause_after_test"], kwargs["pause_on_unexpected"], - kwargs["debug_args"]) as manager_group: + kwargs["debug_info"]) as manager_group: try: manager_group.run(test_type, test_loader.tests) except KeyboardInterrupt: diff --git a/tests/wpt/harness/wptrunner/wpttest.py b/tests/wpt/harness/wptrunner/wpttest.py index 6c6c6bf4e45..d2d19fe5489 100644 --- a/tests/wpt/harness/wptrunner/wpttest.py +++ b/tests/wpt/harness/wptrunner/wpttest.py @@ -90,7 +90,11 @@ class Test(object): self._expected_metadata = expected_metadata self.timeout = timeout self.path = path - self.protocol = protocol + if expected_metadata: + prefs = expected_metadata.prefs() + else: + prefs = [] + self.environment = {"protocol": protocol, "prefs": prefs} def __eq__(self, other): return self.id == other.id @@ -102,7 +106,7 @@ class Test(object): expected_metadata, timeout=timeout, path=manifest_item.path, - protocol="https" if manifest_item.https else "http") + protocol="https" if hasattr(manifest_item, "https") and manifest_item.https else "http") @property @@ -165,14 +169,12 @@ class ReftestTest(Test): result_cls = ReftestResult def __init__(self, url, expected, references, timeout=DEFAULT_TIMEOUT, path=None, protocol="http"): - self.url = url + Test.__init__(self, url, expected, timeout, path, protocol) + for _, ref_type in references: if ref_type not in ("==", "!="): raise ValueError - self._expected_metadata = expected - self.timeout = timeout - self.path = path - self.protocol = protocol + self.references = references @classmethod @@ -196,7 +198,7 @@ class ReftestTest(Test): [], timeout=timeout, path=manifest_test.path, - protocol="https" if manifest_test.https else "http") + protocol="https" if hasattr(manifest_test, "https") and manifest_test.https else "http") nodes[url] = node