From 27e1c626f6c423eebfba70b023acda63de7f86fe Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Tue, 2 Aug 2022 00:33:14 +0000 Subject: [PATCH 1/9] rename browser to runner --- pytest_pyodide/__init__.py | 6 +- pytest_pyodide/browser.py | 541 +------------------------------------ pytest_pyodide/runner.py | 536 ++++++++++++++++++++++++++++++++++++ 3 files changed, 550 insertions(+), 533 deletions(-) create mode 100644 pytest_pyodide/runner.py diff --git a/pytest_pyodide/__init__.py b/pytest_pyodide/__init__.py index 9ec4bd1b..f8068839 100644 --- a/pytest_pyodide/__init__.py +++ b/pytest_pyodide/__init__.py @@ -1,6 +1,8 @@ from importlib.metadata import PackageNotFoundError, version -from .browser import ( +from .decorator import run_in_pyodide +from .fixture import * # noqa: F403, F401 +from .runner import ( BrowserWrapper, NodeWrapper, PlaywrightChromeWrapper, @@ -10,8 +12,6 @@ SeleniumFirefoxWrapper, SeleniumWrapper, ) -from .decorator import run_in_pyodide -from .fixture import * # noqa: F403, F401 from .server import spawn_web_server from .utils import parse_driver_timeout, set_webdriver_script_timeout diff --git a/pytest_pyodide/browser.py b/pytest_pyodide/browser.py index 60cb776d..ca3bfc7a 100644 --- a/pytest_pyodide/browser.py +++ b/pytest_pyodide/browser.py @@ -1,536 +1,17 @@ -import json -import textwrap -from pathlib import Path +import warnings -import pexpect +from . import runner -TEST_SETUP_CODE = """ -Error.stackTraceLimit = Infinity; +warnings.simplefilter("always", DeprecationWarning) +warnings.warn( + "pytest_pyodide.browser has been renamed to the pytest_pyodide.runner", + DeprecationWarning, +) -// Fix globalThis is messed up in firefox see facebook/react#16606. -// Replace it with window. -globalThis.globalThis = globalThis.window || globalThis; -globalThis.sleep = function (s) { - return new Promise((resolve) => setTimeout(resolve, s)); -}; +def __setattr__(name, value): + setattr(runner, name, value) -globalThis.assert = function (cb, message = "") { - if (message !== "") { - message = "\\n" + message; - } - if (cb() !== true) { - throw new Error( - `Assertion failed: ${cb.toString().slice(6)}${message}` - ); - } -}; -globalThis.assertAsync = async function (cb, message = "") { - if (message !== "") { - message = "\\n" + message; - } - if ((await cb()) !== true) { - throw new Error( - `Assertion failed: ${cb.toString().slice(12)}${message}` - ); - } -}; - -function checkError(err, errname, pattern, pat_str, thiscallstr) { - if (typeof pattern === "string") { - pattern = new RegExp(pattern); - } - if (!err) { - throw new Error(`${thiscallstr} failed, no error thrown`); - } - if (err.constructor.name !== errname) { - throw new Error( - `${thiscallstr} failed, expected error ` + - `of type '${errname}' got type '${err.constructor.name}'` - ); - } - if (!pattern.test(err.message)) { - throw new Error( - `${thiscallstr} failed, expected error ` + - `message to match pattern ${pat_str} got:\n${err.message}` - ); - } -} - -globalThis.assertThrows = function (cb, errname, pattern) { - let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`; - let thiscallstr = `assertThrows(${cb.toString()}, "${errname}", ${pat_str})`; - let err = undefined; - try { - cb(); - } catch (e) { - err = e; - } - checkError(err, errname, pattern, pat_str, thiscallstr); -}; - -globalThis.assertThrowsAsync = async function (cb, errname, pattern) { - let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`; - let thiscallstr = `assertThrowsAsync(${cb.toString()}, "${errname}", ${pat_str})`; - let err = undefined; - try { - await cb(); - } catch (e) { - err = e; - } - checkError(err, errname, pattern, pat_str, thiscallstr); -}; -""".strip() - -INITIALIZE_SCRIPT = "pyodide.runPython('');" - - -class JavascriptException(Exception): - def __init__(self, msg, stack): - self.msg = msg - self.stack = stack - # In chrome the stack contains the message - if self.stack and self.stack.startswith(self.msg): - self.msg = "" - - def __str__(self): - return "\n\n".join(x for x in [self.msg, self.stack] if x) - - -class BrowserWrapper: - browser = "" - JavascriptException = JavascriptException - - def __init__( - self, - server_port, - server_hostname="127.0.0.1", - server_log=None, - load_pyodide=True, - script_timeout=20, - script_type="classic", - dist_dir=None, - *args, - **kwargs, - ): - self.server_port = server_port - self.server_hostname = server_hostname - self.base_url = f"http://{self.server_hostname}:{self.server_port}" - self.server_log = server_log - self.script_type = script_type - self.dist_dir = dist_dir - self.driver = self.get_driver() - self.set_script_timeout(script_timeout) - self.script_timeout = script_timeout - self.prepare_driver() - self.javascript_setup() - if load_pyodide: - self.load_pyodide() - self.initialize_pyodide() - self.save_state() - self.restore_state() - - def get_driver(self): - raise NotImplementedError() - - def goto(self, page): - raise NotImplementedError() - - def set_script_timeout(self, timeout): - raise NotImplementedError() - - def quit(self): - raise NotImplementedError() - - def refresh(self): - raise NotImplementedError() - - def run_js_inner(self, code, check_code): - raise NotImplementedError() - - def prepare_driver(self): - if self.script_type == "classic": - self.goto(f"{self.base_url}/test.html") - elif self.script_type == "module": - self.goto(f"{self.base_url}/module_test.html") - else: - raise Exception("Unknown script type to load!") - - def javascript_setup(self): - self.run_js( - TEST_SETUP_CODE, - pyodide_checks=False, - ) - - def load_pyodide(self): - self.run_js( - """ - let pyodide = await loadPyodide({ fullStdLib: false, jsglobals : self }); - self.pyodide = pyodide; - globalThis.pyodide = pyodide; - pyodide._api.inTestHoist = true; // improve some error messages for tests - """ - ) - - def initialize_pyodide(self): - self.run_js(INITIALIZE_SCRIPT) - - @property - def pyodide_loaded(self): - return self.run_js("return !!(self.pyodide && self.pyodide.runPython);") - - @property - def logs(self): - logs = self.run_js("return self.logs;", pyodide_checks=False) - if logs is not None: - return "\n".join(str(x) for x in logs) - return "" - - def clean_logs(self): - self.run_js("self.logs = []", pyodide_checks=False) - - def run(self, code): - return self.run_js( - f""" - let result = pyodide.runPython({code!r}); - if(result && result.toJs){{ - let converted_result = result.toJs(); - if(pyodide.isPyProxy(converted_result)){{ - converted_result = undefined; - }} - result.destroy(); - return converted_result; - }} - return result; - """ - ) - - def run_async(self, code): - return self.run_js( - f""" - await pyodide.loadPackagesFromImports({code!r}) - let result = await pyodide.runPythonAsync({code!r}); - if(result && result.toJs){{ - let converted_result = result.toJs(); - if(pyodide.isPyProxy(converted_result)){{ - converted_result = undefined; - }} - result.destroy(); - return converted_result; - }} - return result; - """ - ) - - def run_js(self, code, pyodide_checks=True): - """Run JavaScript code and check for pyodide errors""" - if isinstance(code, str) and code.startswith("\n"): - # we have a multiline string, fix indentation - code = textwrap.dedent(code) - - if pyodide_checks: - check_code = """ - if(globalThis.pyodide && pyodide._module && pyodide._module._PyErr_Occurred()){ - try { - pyodide._module._pythonexc2js(); - } catch(e){ - console.error(`Python exited with error flag set! Error was:\n${e.message}`); - // Don't put original error message in new one: we want - // "pytest.raises(xxx, match=msg)" to fail - throw new Error(`Python exited with error flag set!`); - } - } - """ - else: - check_code = "" - return self.run_js_inner(code, check_code) - - def get_num_hiwire_keys(self): - return self.run_js("return pyodide._module.hiwire.num_keys();") - - @property - def force_test_fail(self) -> bool: - return self.run_js("return !!pyodide._api.fail_test;") - - def clear_force_test_fail(self): - self.run_js("pyodide._api.fail_test = false;") - - def save_state(self): - self.run_js("self.__savedState = pyodide._api.saveState();") - - def restore_state(self): - self.run_js( - """ - if(self.__savedState){ - pyodide._api.restoreState(self.__savedState) - } - """ - ) - - def get_num_proxies(self): - return self.run_js("return pyodide._module.pyproxy_alloc_map.size") - - def enable_pyproxy_tracing(self): - self.run_js("pyodide._module.enable_pyproxy_allocation_tracing()") - - def disable_pyproxy_tracing(self): - self.run_js("pyodide._module.disable_pyproxy_allocation_tracing()") - - def run_webworker(self, code): - if isinstance(code, str) and code.startswith("\n"): - # we have a multiline string, fix indentation - code = textwrap.dedent(code) - - worker_file = ( - "webworker_dev.js" - if self.script_type == "classic" - else "module_webworker_dev.js" - ) - - return self.run_js( - """ - let worker = new Worker('{}', {{ type: '{}' }}); - let res = new Promise((res, rej) => {{ - worker.onerror = e => rej(e); - worker.onmessage = e => {{ - if (e.data.results) {{ - res(e.data.results); - }} else {{ - rej(e.data.error); - }} - }}; - worker.postMessage({{ python: {!r} }}); - }}); - return await res - """.format( - f"http://{self.server_hostname}:{self.server_port}/{worker_file}", - self.script_type, - code, - ), - pyodide_checks=False, - ) - - def load_package(self, packages): - self.run_js(f"await pyodide.loadPackage({packages!r})") - - -class SeleniumWrapper(BrowserWrapper): - def goto(self, page): - self.driver.get(page) - - def set_script_timeout(self, timeout): - self.driver.set_script_timeout(timeout) - - def quit(self): - self.driver.quit() - - def refresh(self): - self.driver.refresh() - self.javascript_setup() - - def run_js_inner(self, code, check_code): - wrapper = """ - let cb = arguments[arguments.length - 1]; - let run = async () => { %s } - (async () => { - try { - let result = await run(); - %s - cb([0, result]); - } catch (e) { - cb([1, e.toString(), e.stack, e.message]); - } - })() - """ - retval = self.driver.execute_async_script(wrapper % (code, check_code)) - if retval[0] == 0: - return retval[1] - else: - print("JavascriptException message: ", retval[3]) - raise JavascriptException(retval[1], retval[2]) - - @property - def urls(self): - for handle in self.driver.window_handles: - self.driver.switch_to.window(handle) - yield self.driver.current_url - - -class PlaywrightWrapper(BrowserWrapper): - def __init__(self, browsers, *args, **kwargs): - self.browsers = browsers - super().__init__(*args, **kwargs) - - def goto(self, page): - self.driver.goto(page) - - def get_driver(self): - return self.browsers[self.browser].new_page() - - def set_script_timeout(self, timeout): - # playwright uses milliseconds for timeout - self.driver.set_default_timeout(timeout * 1000) - - def quit(self): - self.driver.close() - - def refresh(self): - self.driver.reload() - self.javascript_setup() - - def run_js_inner(self, code, check_code): - # playwright `evaluate` waits until primise to resolve, - # so we don't need to use a callback like selenium. - wrapper = """ - let run = async () => { %s } - (async () => { - try { - let result = await run(); - %s - return [0, result]; - } catch (e) { - return [1, e.toString(), e.stack]; - } - })() - """ - retval = self.driver.evaluate(wrapper % (code, check_code)) - if retval[0] == 0: - return retval[1] - else: - raise JavascriptException(retval[1], retval[2]) - - -class SeleniumFirefoxWrapper(SeleniumWrapper): - - browser = "firefox" - - def get_driver(self): - from selenium.webdriver import Firefox - from selenium.webdriver.firefox.options import Options - - options = Options() - options.add_argument("--headless") - - return Firefox(executable_path="geckodriver", options=options) - - -class SeleniumChromeWrapper(SeleniumWrapper): - - browser = "chrome" - - def get_driver(self): - from selenium.webdriver import Chrome - from selenium.webdriver.chrome.options import Options - - options = Options() - options.add_argument("--headless") - options.add_argument("--no-sandbox") - options.add_argument("--js-flags=--expose-gc") - return Chrome(options=options) - - def collect_garbage(self): - self.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {}) - - -class PlaywrightChromeWrapper(PlaywrightWrapper): - browser = "chrome" - - def collect_garbage(self): - client = self.driver.context.new_cdp_session(self.driver) - client.send("HeapProfiler.collectGarbage") - - -class PlaywrightFirefoxWrapper(PlaywrightWrapper): - browser = "firefox" - - -class NodeWrapper(BrowserWrapper): - browser = "node" - - def init_node(self): - curdir = Path(__file__).parent - self.p = pexpect.spawn("/bin/bash", timeout=60) - self.p.setecho(False) - self.p.delaybeforesend = None - # disable canonical input processing mode to allow sending longer lines - # See: https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn.send - self.p.sendline("stty -icanon") - - node_version = pexpect.spawn("node --version").read().decode("utf-8") - node_extra_args = "" - # Node v14 require the --experimental-wasm-bigint which - # produces errors on later versions - if node_version.startswith("v14"): - node_extra_args = "--experimental-wasm-bigint" - - self.p.sendline( - f"node --expose-gc {node_extra_args} {curdir}/node_test_driver.js {self.base_url} {self.dist_dir}", - ) - - try: - self.p.expect_exact("READY!!") - except (pexpect.exceptions.EOF, pexpect.exceptions.TIMEOUT): - raise JavascriptException("", self.p.before.decode()) - - def get_driver(self): - self._logs = [] - self.init_node() - - class NodeDriver: - def __getattr__(self, x): - raise NotImplementedError() - - return NodeDriver() - - def prepare_driver(self): - pass - - def set_script_timeout(self, timeout): - self._timeout = timeout - - def quit(self): - self.p.sendeof() - - def refresh(self): - self.quit() - self.init_node() - self.javascript_setup() - - def collect_garbage(self): - self.run_js("gc()") - - @property - def logs(self): - return "\n".join(self._logs) - - def clean_logs(self): - self._logs = [] - - def run_js_inner(self, code, check_code): - check_code = "" - wrapped = """ - let result = await (async () => {{ {} }})(); - {} - return result; - """.format( - code, - check_code, - ) - from uuid import uuid4 - - cmd_id = str(uuid4()) - self.p.sendline(cmd_id) - self.p.sendline(wrapped) - self.p.sendline(cmd_id) - self.p.expect_exact(f"{cmd_id}:UUID\r\n", timeout=self._timeout) - self.p.expect_exact(f"{cmd_id}:UUID\r\n") - if self.p.before: - self._logs.append(self.p.before.decode()[:-2].replace("\r", "")) - self.p.expect("[01]\r\n") - success = int(self.p.match[0].decode()[0]) == 0 - self.p.expect_exact(f"\r\n{cmd_id}:UUID\r\n") - if success: - return json.loads(self.p.before.decode().replace("undefined", "null")) - else: - raise JavascriptException("", self.p.before.decode()) +def __getattr__(name): + return getattr(runner, name) diff --git a/pytest_pyodide/runner.py b/pytest_pyodide/runner.py new file mode 100644 index 00000000..60cb776d --- /dev/null +++ b/pytest_pyodide/runner.py @@ -0,0 +1,536 @@ +import json +import textwrap +from pathlib import Path + +import pexpect + +TEST_SETUP_CODE = """ +Error.stackTraceLimit = Infinity; + +// Fix globalThis is messed up in firefox see facebook/react#16606. +// Replace it with window. +globalThis.globalThis = globalThis.window || globalThis; + +globalThis.sleep = function (s) { + return new Promise((resolve) => setTimeout(resolve, s)); +}; + +globalThis.assert = function (cb, message = "") { + if (message !== "") { + message = "\\n" + message; + } + if (cb() !== true) { + throw new Error( + `Assertion failed: ${cb.toString().slice(6)}${message}` + ); + } +}; + +globalThis.assertAsync = async function (cb, message = "") { + if (message !== "") { + message = "\\n" + message; + } + if ((await cb()) !== true) { + throw new Error( + `Assertion failed: ${cb.toString().slice(12)}${message}` + ); + } +}; + +function checkError(err, errname, pattern, pat_str, thiscallstr) { + if (typeof pattern === "string") { + pattern = new RegExp(pattern); + } + if (!err) { + throw new Error(`${thiscallstr} failed, no error thrown`); + } + if (err.constructor.name !== errname) { + throw new Error( + `${thiscallstr} failed, expected error ` + + `of type '${errname}' got type '${err.constructor.name}'` + ); + } + if (!pattern.test(err.message)) { + throw new Error( + `${thiscallstr} failed, expected error ` + + `message to match pattern ${pat_str} got:\n${err.message}` + ); + } +} + +globalThis.assertThrows = function (cb, errname, pattern) { + let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`; + let thiscallstr = `assertThrows(${cb.toString()}, "${errname}", ${pat_str})`; + let err = undefined; + try { + cb(); + } catch (e) { + err = e; + } + checkError(err, errname, pattern, pat_str, thiscallstr); +}; + +globalThis.assertThrowsAsync = async function (cb, errname, pattern) { + let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`; + let thiscallstr = `assertThrowsAsync(${cb.toString()}, "${errname}", ${pat_str})`; + let err = undefined; + try { + await cb(); + } catch (e) { + err = e; + } + checkError(err, errname, pattern, pat_str, thiscallstr); +}; +""".strip() + +INITIALIZE_SCRIPT = "pyodide.runPython('');" + + +class JavascriptException(Exception): + def __init__(self, msg, stack): + self.msg = msg + self.stack = stack + # In chrome the stack contains the message + if self.stack and self.stack.startswith(self.msg): + self.msg = "" + + def __str__(self): + return "\n\n".join(x for x in [self.msg, self.stack] if x) + + +class BrowserWrapper: + browser = "" + JavascriptException = JavascriptException + + def __init__( + self, + server_port, + server_hostname="127.0.0.1", + server_log=None, + load_pyodide=True, + script_timeout=20, + script_type="classic", + dist_dir=None, + *args, + **kwargs, + ): + self.server_port = server_port + self.server_hostname = server_hostname + self.base_url = f"http://{self.server_hostname}:{self.server_port}" + self.server_log = server_log + self.script_type = script_type + self.dist_dir = dist_dir + self.driver = self.get_driver() + self.set_script_timeout(script_timeout) + self.script_timeout = script_timeout + self.prepare_driver() + self.javascript_setup() + if load_pyodide: + self.load_pyodide() + self.initialize_pyodide() + self.save_state() + self.restore_state() + + def get_driver(self): + raise NotImplementedError() + + def goto(self, page): + raise NotImplementedError() + + def set_script_timeout(self, timeout): + raise NotImplementedError() + + def quit(self): + raise NotImplementedError() + + def refresh(self): + raise NotImplementedError() + + def run_js_inner(self, code, check_code): + raise NotImplementedError() + + def prepare_driver(self): + if self.script_type == "classic": + self.goto(f"{self.base_url}/test.html") + elif self.script_type == "module": + self.goto(f"{self.base_url}/module_test.html") + else: + raise Exception("Unknown script type to load!") + + def javascript_setup(self): + self.run_js( + TEST_SETUP_CODE, + pyodide_checks=False, + ) + + def load_pyodide(self): + self.run_js( + """ + let pyodide = await loadPyodide({ fullStdLib: false, jsglobals : self }); + self.pyodide = pyodide; + globalThis.pyodide = pyodide; + pyodide._api.inTestHoist = true; // improve some error messages for tests + """ + ) + + def initialize_pyodide(self): + self.run_js(INITIALIZE_SCRIPT) + + @property + def pyodide_loaded(self): + return self.run_js("return !!(self.pyodide && self.pyodide.runPython);") + + @property + def logs(self): + logs = self.run_js("return self.logs;", pyodide_checks=False) + if logs is not None: + return "\n".join(str(x) for x in logs) + return "" + + def clean_logs(self): + self.run_js("self.logs = []", pyodide_checks=False) + + def run(self, code): + return self.run_js( + f""" + let result = pyodide.runPython({code!r}); + if(result && result.toJs){{ + let converted_result = result.toJs(); + if(pyodide.isPyProxy(converted_result)){{ + converted_result = undefined; + }} + result.destroy(); + return converted_result; + }} + return result; + """ + ) + + def run_async(self, code): + return self.run_js( + f""" + await pyodide.loadPackagesFromImports({code!r}) + let result = await pyodide.runPythonAsync({code!r}); + if(result && result.toJs){{ + let converted_result = result.toJs(); + if(pyodide.isPyProxy(converted_result)){{ + converted_result = undefined; + }} + result.destroy(); + return converted_result; + }} + return result; + """ + ) + + def run_js(self, code, pyodide_checks=True): + """Run JavaScript code and check for pyodide errors""" + if isinstance(code, str) and code.startswith("\n"): + # we have a multiline string, fix indentation + code = textwrap.dedent(code) + + if pyodide_checks: + check_code = """ + if(globalThis.pyodide && pyodide._module && pyodide._module._PyErr_Occurred()){ + try { + pyodide._module._pythonexc2js(); + } catch(e){ + console.error(`Python exited with error flag set! Error was:\n${e.message}`); + // Don't put original error message in new one: we want + // "pytest.raises(xxx, match=msg)" to fail + throw new Error(`Python exited with error flag set!`); + } + } + """ + else: + check_code = "" + return self.run_js_inner(code, check_code) + + def get_num_hiwire_keys(self): + return self.run_js("return pyodide._module.hiwire.num_keys();") + + @property + def force_test_fail(self) -> bool: + return self.run_js("return !!pyodide._api.fail_test;") + + def clear_force_test_fail(self): + self.run_js("pyodide._api.fail_test = false;") + + def save_state(self): + self.run_js("self.__savedState = pyodide._api.saveState();") + + def restore_state(self): + self.run_js( + """ + if(self.__savedState){ + pyodide._api.restoreState(self.__savedState) + } + """ + ) + + def get_num_proxies(self): + return self.run_js("return pyodide._module.pyproxy_alloc_map.size") + + def enable_pyproxy_tracing(self): + self.run_js("pyodide._module.enable_pyproxy_allocation_tracing()") + + def disable_pyproxy_tracing(self): + self.run_js("pyodide._module.disable_pyproxy_allocation_tracing()") + + def run_webworker(self, code): + if isinstance(code, str) and code.startswith("\n"): + # we have a multiline string, fix indentation + code = textwrap.dedent(code) + + worker_file = ( + "webworker_dev.js" + if self.script_type == "classic" + else "module_webworker_dev.js" + ) + + return self.run_js( + """ + let worker = new Worker('{}', {{ type: '{}' }}); + let res = new Promise((res, rej) => {{ + worker.onerror = e => rej(e); + worker.onmessage = e => {{ + if (e.data.results) {{ + res(e.data.results); + }} else {{ + rej(e.data.error); + }} + }}; + worker.postMessage({{ python: {!r} }}); + }}); + return await res + """.format( + f"http://{self.server_hostname}:{self.server_port}/{worker_file}", + self.script_type, + code, + ), + pyodide_checks=False, + ) + + def load_package(self, packages): + self.run_js(f"await pyodide.loadPackage({packages!r})") + + +class SeleniumWrapper(BrowserWrapper): + def goto(self, page): + self.driver.get(page) + + def set_script_timeout(self, timeout): + self.driver.set_script_timeout(timeout) + + def quit(self): + self.driver.quit() + + def refresh(self): + self.driver.refresh() + self.javascript_setup() + + def run_js_inner(self, code, check_code): + wrapper = """ + let cb = arguments[arguments.length - 1]; + let run = async () => { %s } + (async () => { + try { + let result = await run(); + %s + cb([0, result]); + } catch (e) { + cb([1, e.toString(), e.stack, e.message]); + } + })() + """ + retval = self.driver.execute_async_script(wrapper % (code, check_code)) + if retval[0] == 0: + return retval[1] + else: + print("JavascriptException message: ", retval[3]) + raise JavascriptException(retval[1], retval[2]) + + @property + def urls(self): + for handle in self.driver.window_handles: + self.driver.switch_to.window(handle) + yield self.driver.current_url + + +class PlaywrightWrapper(BrowserWrapper): + def __init__(self, browsers, *args, **kwargs): + self.browsers = browsers + super().__init__(*args, **kwargs) + + def goto(self, page): + self.driver.goto(page) + + def get_driver(self): + return self.browsers[self.browser].new_page() + + def set_script_timeout(self, timeout): + # playwright uses milliseconds for timeout + self.driver.set_default_timeout(timeout * 1000) + + def quit(self): + self.driver.close() + + def refresh(self): + self.driver.reload() + self.javascript_setup() + + def run_js_inner(self, code, check_code): + # playwright `evaluate` waits until primise to resolve, + # so we don't need to use a callback like selenium. + wrapper = """ + let run = async () => { %s } + (async () => { + try { + let result = await run(); + %s + return [0, result]; + } catch (e) { + return [1, e.toString(), e.stack]; + } + })() + """ + retval = self.driver.evaluate(wrapper % (code, check_code)) + if retval[0] == 0: + return retval[1] + else: + raise JavascriptException(retval[1], retval[2]) + + +class SeleniumFirefoxWrapper(SeleniumWrapper): + + browser = "firefox" + + def get_driver(self): + from selenium.webdriver import Firefox + from selenium.webdriver.firefox.options import Options + + options = Options() + options.add_argument("--headless") + + return Firefox(executable_path="geckodriver", options=options) + + +class SeleniumChromeWrapper(SeleniumWrapper): + + browser = "chrome" + + def get_driver(self): + from selenium.webdriver import Chrome + from selenium.webdriver.chrome.options import Options + + options = Options() + options.add_argument("--headless") + options.add_argument("--no-sandbox") + options.add_argument("--js-flags=--expose-gc") + return Chrome(options=options) + + def collect_garbage(self): + self.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {}) + + +class PlaywrightChromeWrapper(PlaywrightWrapper): + browser = "chrome" + + def collect_garbage(self): + client = self.driver.context.new_cdp_session(self.driver) + client.send("HeapProfiler.collectGarbage") + + +class PlaywrightFirefoxWrapper(PlaywrightWrapper): + browser = "firefox" + + +class NodeWrapper(BrowserWrapper): + browser = "node" + + def init_node(self): + curdir = Path(__file__).parent + self.p = pexpect.spawn("/bin/bash", timeout=60) + self.p.setecho(False) + self.p.delaybeforesend = None + # disable canonical input processing mode to allow sending longer lines + # See: https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn.send + self.p.sendline("stty -icanon") + + node_version = pexpect.spawn("node --version").read().decode("utf-8") + node_extra_args = "" + # Node v14 require the --experimental-wasm-bigint which + # produces errors on later versions + if node_version.startswith("v14"): + node_extra_args = "--experimental-wasm-bigint" + + self.p.sendline( + f"node --expose-gc {node_extra_args} {curdir}/node_test_driver.js {self.base_url} {self.dist_dir}", + ) + + try: + self.p.expect_exact("READY!!") + except (pexpect.exceptions.EOF, pexpect.exceptions.TIMEOUT): + raise JavascriptException("", self.p.before.decode()) + + def get_driver(self): + self._logs = [] + self.init_node() + + class NodeDriver: + def __getattr__(self, x): + raise NotImplementedError() + + return NodeDriver() + + def prepare_driver(self): + pass + + def set_script_timeout(self, timeout): + self._timeout = timeout + + def quit(self): + self.p.sendeof() + + def refresh(self): + self.quit() + self.init_node() + self.javascript_setup() + + def collect_garbage(self): + self.run_js("gc()") + + @property + def logs(self): + return "\n".join(self._logs) + + def clean_logs(self): + self._logs = [] + + def run_js_inner(self, code, check_code): + check_code = "" + wrapped = """ + let result = await (async () => {{ {} }})(); + {} + return result; + """.format( + code, + check_code, + ) + from uuid import uuid4 + + cmd_id = str(uuid4()) + self.p.sendline(cmd_id) + self.p.sendline(wrapped) + self.p.sendline(cmd_id) + self.p.expect_exact(f"{cmd_id}:UUID\r\n", timeout=self._timeout) + self.p.expect_exact(f"{cmd_id}:UUID\r\n") + if self.p.before: + self._logs.append(self.p.before.decode()[:-2].replace("\r", "")) + self.p.expect("[01]\r\n") + success = int(self.p.match[0].decode()[0]) == 0 + self.p.expect_exact(f"\r\n{cmd_id}:UUID\r\n") + if success: + return json.loads(self.p.before.decode().replace("undefined", "null")) + else: + raise JavascriptException("", self.p.before.decode()) From 66ecc9a7c4e44684beee08a61d6d6a5077724083 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Tue, 2 Aug 2022 00:33:57 +0000 Subject: [PATCH 2/9] Remove import * --- pytest_pyodide/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest_pyodide/__init__.py b/pytest_pyodide/__init__.py index f8068839..1370dda6 100644 --- a/pytest_pyodide/__init__.py +++ b/pytest_pyodide/__init__.py @@ -1,7 +1,6 @@ from importlib.metadata import PackageNotFoundError, version from .decorator import run_in_pyodide -from .fixture import * # noqa: F403, F401 from .runner import ( BrowserWrapper, NodeWrapper, From 9bdc75d395ae64932fb490f39b153e4d6752f9ed Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Tue, 2 Aug 2022 01:04:47 +0000 Subject: [PATCH 3/9] Rename warpper to runners --- pytest_pyodide/__init__.py | 21 ++++++++++++++----- pytest_pyodide/fixture.py | 28 +++++++++++++------------- pytest_pyodide/runner.py | 41 ++++++++++++++++++++++++++++++-------- tests/test_testing.py | 10 ++++++++++ 4 files changed, 73 insertions(+), 27 deletions(-) diff --git a/pytest_pyodide/__init__.py b/pytest_pyodide/__init__.py index 1370dda6..415170f8 100644 --- a/pytest_pyodide/__init__.py +++ b/pytest_pyodide/__init__.py @@ -1,13 +1,18 @@ from importlib.metadata import PackageNotFoundError, version from .decorator import run_in_pyodide -from .runner import ( +from .runner import ( # deprecated BrowserWrapper, + NodeRunner, NodeWrapper, + PlaywrightChromeRunner, PlaywrightChromeWrapper, + PlaywrightFirefoxRunner, PlaywrightFirefoxWrapper, PlaywrightWrapper, + SeleniumChromeRunner, SeleniumChromeWrapper, + SeleniumFirefoxRunner, SeleniumFirefoxWrapper, SeleniumWrapper, ) @@ -21,6 +26,16 @@ pass __all__ = [ + "NodeRunner", + "PlaywrightChromeRunner", + "PlaywrightFirefoxRunner", + "SeleniumChromeRunner", + "SeleniumFirefoxRunner", + "set_webdriver_script_timeout", + "parse_driver_timeout", + "run_in_pyodide", + "spawn_web_server", + # deprecated "BrowserWrapper", "SeleniumWrapper", "PlaywrightWrapper", @@ -29,8 +44,4 @@ "PlaywrightChromeWrapper", "PlaywrightFirefoxWrapper", "NodeWrapper", - "set_webdriver_script_timeout", - "parse_driver_timeout", - "run_in_pyodide", - "spawn_web_server", ] diff --git a/pytest_pyodide/fixture.py b/pytest_pyodide/fixture.py index 75567819..4638ce50 100644 --- a/pytest_pyodide/fixture.py +++ b/pytest_pyodide/fixture.py @@ -4,13 +4,13 @@ import pytest -from .browser import ( - BrowserWrapper, - NodeWrapper, - PlaywrightChromeWrapper, - PlaywrightFirefoxWrapper, - SeleniumChromeWrapper, - SeleniumFirefoxWrapper, +from .runner import ( + NodeRunner, + PlaywrightChromeRunner, + PlaywrightFirefoxRunner, + SeleniumChromeRunner, + SeleniumFirefoxRunner, + _BrowserBaseRunner, ) from .server import spawn_web_server from .utils import parse_driver_timeout, set_webdriver_script_timeout @@ -70,15 +70,15 @@ def selenium_common( server_hostname, server_port, server_log = web_server_main runner_type = request.config.option.runner.lower() - cls: type[BrowserWrapper] + cls: type[_BrowserBaseRunner] browser_set = { - ("selenium", "firefox"): SeleniumFirefoxWrapper, - ("selenium", "chrome"): SeleniumChromeWrapper, - ("selenium", "node"): NodeWrapper, - ("playwright", "firefox"): PlaywrightFirefoxWrapper, - ("playwright", "chrome"): PlaywrightChromeWrapper, - ("playwright", "node"): NodeWrapper, + ("selenium", "firefox"): SeleniumFirefoxRunner, + ("selenium", "chrome"): SeleniumChromeRunner, + ("selenium", "node"): NodeRunner, + ("playwright", "firefox"): PlaywrightFirefoxRunner, + ("playwright", "chrome"): PlaywrightChromeRunner, + ("playwright", "node"): NodeRunner, } cls = browser_set.get((runner_type, runtime)) # type: ignore[assignment] diff --git a/pytest_pyodide/runner.py b/pytest_pyodide/runner.py index 60cb776d..1080447c 100644 --- a/pytest_pyodide/runner.py +++ b/pytest_pyodide/runner.py @@ -98,7 +98,7 @@ def __str__(self): return "\n\n".join(x for x in [self.msg, self.stack] if x) -class BrowserWrapper: +class _BrowserBaseRunner: browser = "" JavascriptException = JavascriptException @@ -315,7 +315,7 @@ def load_package(self, packages): self.run_js(f"await pyodide.loadPackage({packages!r})") -class SeleniumWrapper(BrowserWrapper): +class _SeleniumBaseRunner(_BrowserBaseRunner): def goto(self, page): self.driver.get(page) @@ -357,7 +357,7 @@ def urls(self): yield self.driver.current_url -class PlaywrightWrapper(BrowserWrapper): +class _PlaywrightBaseRunner(_BrowserBaseRunner): def __init__(self, browsers, *args, **kwargs): self.browsers = browsers super().__init__(*args, **kwargs) @@ -401,7 +401,7 @@ def run_js_inner(self, code, check_code): raise JavascriptException(retval[1], retval[2]) -class SeleniumFirefoxWrapper(SeleniumWrapper): +class SeleniumFirefoxRunner(_SeleniumBaseRunner): browser = "firefox" @@ -415,7 +415,7 @@ def get_driver(self): return Firefox(executable_path="geckodriver", options=options) -class SeleniumChromeWrapper(SeleniumWrapper): +class SeleniumChromeRunner(_SeleniumBaseRunner): browser = "chrome" @@ -433,7 +433,7 @@ def collect_garbage(self): self.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {}) -class PlaywrightChromeWrapper(PlaywrightWrapper): +class PlaywrightChromeRunner(_PlaywrightBaseRunner): browser = "chrome" def collect_garbage(self): @@ -441,11 +441,11 @@ def collect_garbage(self): client.send("HeapProfiler.collectGarbage") -class PlaywrightFirefoxWrapper(PlaywrightWrapper): +class PlaywrightFirefoxRunner(_PlaywrightBaseRunner): browser = "firefox" -class NodeWrapper(BrowserWrapper): +class NodeRunner(_BrowserBaseRunner): browser = "node" def init_node(self): @@ -534,3 +534,28 @@ def run_js_inner(self, code, check_code): return json.loads(self.p.before.decode().replace("undefined", "null")) else: raise JavascriptException("", self.p.before.decode()) + + +def _deprecated(old_name, new_module): + def _warn_deprecated(*args, **kwargs): + import warnings + + warnings.simplefilter("always", DeprecationWarning) + warnings.warn( + f"{old_name} has been renamed to {new_module.__name__}", DeprecationWarning + ) + return new_module(*args, **kwargs) + + return _warn_deprecated + + +BrowserWrapper = _deprecated("BrowserWrapper", _BrowserBaseRunner) +SeleniumWrapper = _deprecated("SeleniumWrapper", _SeleniumBaseRunner) +PlaywrightWrapper = _deprecated("PlaywrightWrapper", _PlaywrightBaseRunner) +NodeWrapper = _deprecated("NodeWrapper", NodeRunner) +SeleniumChromeWrapper = _deprecated("SeleniumChromeWrapper", SeleniumChromeRunner) +SeleniumFirefoxWrapper = _deprecated("SeleniumFirefoxWrapper", SeleniumFirefoxRunner) +PlaywrightChromeWrapper = _deprecated("PlaywrightChromeWrapper", PlaywrightChromeRunner) +PlaywrightFirefoxWrapper = _deprecated( + "PlaywrightFirefoxWrapper", PlaywrightFirefoxRunner +) diff --git a/tests/test_testing.py b/tests/test_testing.py index 710d64d6..9d22e617 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,5 +1,7 @@ import pathlib +import pytest + def test_web_server_secondary(selenium, web_server_secondary): host, port, logs = web_server_secondary @@ -23,3 +25,11 @@ def test_doctest(): 2 """ pass + + +def test_deprecated(): + import warnings + + warnings.simplefilter("always", DeprecationWarning) + with pytest.warns(DeprecationWarning): + import pytest_pyodide.browser # noqa From cb6bd8629f3da2a17b9715386a1da78a452634b0 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Tue, 2 Aug 2022 07:44:37 +0000 Subject: [PATCH 4/9] Add safari runner for playwright --- .github/workflows/main.yml | 1 + pytest_pyodide/__init__.py | 2 + pytest_pyodide/fixture.py | 94 ++++++++++++++++++++++---------------- pytest_pyodide/hook.py | 5 +- pytest_pyodide/runner.py | 10 ++-- pytest_pyodide/utils.py | 4 +- 6 files changed, 72 insertions(+), 44 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 22e1e235..aa3dd4db 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -60,6 +60,7 @@ jobs: # playwright browser versions are pinned to playwright version {runner: playwright, runtime: firefox, playwright-version: 1.22.0, node-version: 18}, {runner: playwright, runtime: chrome, playwright-version: 1.22.0, node-version: 18}, + {runner: playwright, runtime: safari, playwright-version: 1.22.0, node-version: 18}, ] steps: - uses: actions/checkout@v2 diff --git a/pytest_pyodide/__init__.py b/pytest_pyodide/__init__.py index 415170f8..a4aa4afa 100644 --- a/pytest_pyodide/__init__.py +++ b/pytest_pyodide/__init__.py @@ -9,6 +9,7 @@ PlaywrightChromeWrapper, PlaywrightFirefoxRunner, PlaywrightFirefoxWrapper, + PlaywrightSafariRunner, PlaywrightWrapper, SeleniumChromeRunner, SeleniumChromeWrapper, @@ -29,6 +30,7 @@ "NodeRunner", "PlaywrightChromeRunner", "PlaywrightFirefoxRunner", + "PlaywrightSafariRunner", "SeleniumChromeRunner", "SeleniumFirefoxRunner", "set_webdriver_script_timeout", diff --git a/pytest_pyodide/fixture.py b/pytest_pyodide/fixture.py index 4638ce50..36cbb3a4 100644 --- a/pytest_pyodide/fixture.py +++ b/pytest_pyodide/fixture.py @@ -8,6 +8,7 @@ NodeRunner, PlaywrightChromeRunner, PlaywrightFirefoxRunner, + PlaywrightSafariRunner, SeleniumChromeRunner, SeleniumFirefoxRunner, _BrowserBaseRunner, @@ -16,10 +17,14 @@ from .utils import parse_driver_timeout, set_webdriver_script_timeout +# FIXME: Using `session` scope can reduce the number of playwright context generation. +# However, generating too many browser contexts in a single playwright context +# sometimes hang when closing the context. @pytest.fixture(scope="module") -def playwright_browsers(request): +def playwright_session(request): if request.config.option.runner.lower() != "playwright": - yield {} + yield None + else: # import playwright here to allow running tests without playwright installation try: @@ -30,27 +35,37 @@ def playwright_browsers(request): returncode=1, ) - with sync_playwright() as p: - try: - chromium = p.chromium.launch( - args=[ - "--js-flags=--expose-gc", - ], - ) - firefox = p.firefox.launch() - # webkit = p.webkit.launch() - except Exception as e: - pytest.exit(f"playwright failed to launch\n{e}", returncode=1) - try: - yield { - "chrome": chromium, - "firefox": firefox, - # "webkit": webkit, - } - finally: - chromium.close() - firefox.close() - # webkit.close() + p = sync_playwright().start() + yield p + p.stop() + + +@pytest.fixture(scope="module") +def playwright_browser(request, playwright_session, runtime): + if request.config.option.runner.lower() != "playwright": + yield None + else: + try: + match runtime: + case "chrome": + browser = playwright_session.chromium.launch( + args=[ + "--js-flags=--expose-gc", + ], + ) + case "firefox": + browser = playwright_session.firefox.launch() + case "safari": + browser = playwright_session.webkit.launch() + case "node": + browser = None + except Exception as e: + pytest.exit(f"playwright failed to launch\n{e}", returncode=1) + try: + yield browser + finally: + if browser is not None: + browser.close() @contextlib.contextmanager @@ -60,7 +75,7 @@ def selenium_common( web_server_main, load_pyodide=True, script_type="classic", - browsers=None, + playwright_browser=None, ): """Returns an initialized selenium object. @@ -78,6 +93,7 @@ def selenium_common( ("selenium", "node"): NodeRunner, ("playwright", "firefox"): PlaywrightFirefoxRunner, ("playwright", "chrome"): PlaywrightChromeRunner, + ("playwright", "safari"): PlaywrightSafariRunner, ("playwright", "node"): NodeRunner, } @@ -91,7 +107,7 @@ def selenium_common( server_hostname=server_hostname, server_log=server_log, load_pyodide=load_pyodide, - browsers=browsers, + playwright_browser=playwright_browser, script_type=script_type, dist_dir=dist_dir, ) @@ -102,9 +118,9 @@ def selenium_common( @pytest.fixture(scope="function") -def selenium_standalone(request, runtime, web_server_main, playwright_browsers): +def selenium_standalone(request, runtime, web_server_main, playwright_browser): with selenium_common( - request, runtime, web_server_main, browsers=playwright_browsers + request, runtime, web_server_main, playwright_browser=playwright_browser ) as selenium: with set_webdriver_script_timeout( selenium, script_timeout=parse_driver_timeout(request.node) @@ -116,13 +132,13 @@ def selenium_standalone(request, runtime, web_server_main, playwright_browsers): @pytest.fixture(scope="module") -def selenium_esm(request, runtime, web_server_main, playwright_browsers): +def selenium_esm(request, runtime, web_server_main, playwright_browser): with selenium_common( request, runtime, web_server_main, load_pyodide=True, - browsers=playwright_browsers, + playwright_browser=playwright_browser, script_type="module", ) as selenium: with set_webdriver_script_timeout( @@ -136,14 +152,14 @@ def selenium_esm(request, runtime, web_server_main, playwright_browsers): @contextlib.contextmanager def selenium_standalone_noload_common( - request, runtime, web_server_main, playwright_browsers, script_type="classic" + request, runtime, web_server_main, playwright_browser, script_type="classic" ): with selenium_common( request, runtime, web_server_main, load_pyodide=False, - browsers=playwright_browsers, + playwright_browser=playwright_browser, script_type=script_type, ) as selenium: with set_webdriver_script_timeout( @@ -157,7 +173,7 @@ def selenium_standalone_noload_common( @pytest.fixture(scope="function") def selenium_webworker_standalone( - request, runtime, web_server_main, playwright_browsers, script_type + request, runtime, web_server_main, playwright_browser, script_type ): # Avoid loading the fixture if the test is going to be skipped if runtime == "firefox" and script_type == "module": @@ -167,27 +183,27 @@ def selenium_webworker_standalone( pytest.skip("no support in node") with selenium_standalone_noload_common( - request, runtime, web_server_main, playwright_browsers, script_type=script_type + request, runtime, web_server_main, playwright_browser, script_type=script_type ) as selenium: yield selenium @pytest.fixture(scope="function") -def selenium_standalone_noload(request, runtime, web_server_main, playwright_browsers): +def selenium_standalone_noload(request, runtime, web_server_main, playwright_browser): """Only difference between this and selenium_webworker_standalone is that this also tests on node.""" with selenium_standalone_noload_common( - request, runtime, web_server_main, playwright_browsers + request, runtime, web_server_main, playwright_browser ) as selenium: yield selenium # selenium instance cached at the module level @pytest.fixture(scope="module") -def selenium_module_scope(request, runtime, web_server_main, playwright_browsers): +def selenium_module_scope(request, runtime, web_server_main, playwright_browser): with selenium_common( - request, runtime, web_server_main, browsers=playwright_browsers + request, runtime, web_server_main, playwright_browser=playwright_browser ) as selenium: yield selenium @@ -214,7 +230,7 @@ def selenium(request, selenium_module_scope): @pytest.fixture(scope="function") -def console_html_fixture(request, runtime, web_server_main, playwright_browsers): +def console_html_fixture(request, runtime, web_server_main, playwright_browser): if runtime == "node": pytest.skip("no support in node") @@ -224,7 +240,7 @@ def console_html_fixture(request, runtime, web_server_main, playwright_browsers) runtime, web_server_main, load_pyodide=False, - browsers=playwright_browsers, + playwright_browser=playwright_browser, ) as selenium: selenium.goto( f"http://{selenium.server_hostname}:{selenium.server_port}/console.html" diff --git a/pytest_pyodide/hook.py b/pytest_pyodide/hook.py index d0c542f8..82083037 100644 --- a/pytest_pyodide/hook.py +++ b/pytest_pyodide/hook.py @@ -10,7 +10,7 @@ pytest_pycollect_makemodule as orig_pytest_pycollect_makemodule, ) -RUNTIMES = ["firefox", "chrome", "node"] +RUNTIMES = ["firefox", "chrome", "node", "safari"] def pytest_configure(config): @@ -104,9 +104,12 @@ def pytest_pycollect_makemodule(module_path: Path, path: Any, parent: Any) -> No def pytest_generate_tests(metafunc: Any) -> None: if "runtime" in metafunc.fixturenames: runtime = metafunc.config.option.runtime + runner = metafunc.config.option.runner if runtime == "all": runtime = RUNTIMES + if runner == "selenium": + runtime = runtime.remove("safari") metafunc.parametrize("runtime", [runtime], scope="module") diff --git a/pytest_pyodide/runner.py b/pytest_pyodide/runner.py index 1080447c..a563abfd 100644 --- a/pytest_pyodide/runner.py +++ b/pytest_pyodide/runner.py @@ -358,15 +358,15 @@ def urls(self): class _PlaywrightBaseRunner(_BrowserBaseRunner): - def __init__(self, browsers, *args, **kwargs): - self.browsers = browsers + def __init__(self, playwright_browser, *args, **kwargs): + self.playwright_browser = playwright_browser super().__init__(*args, **kwargs) def goto(self, page): self.driver.goto(page) def get_driver(self): - return self.browsers[self.browser].new_page() + return self.playwright_browser.new_page() def set_script_timeout(self, timeout): # playwright uses milliseconds for timeout @@ -445,6 +445,10 @@ class PlaywrightFirefoxRunner(_PlaywrightBaseRunner): browser = "firefox" +class PlaywrightSafariRunner(_PlaywrightBaseRunner): + browser = "safari" + + class NodeRunner(_BrowserBaseRunner): browser = "node" diff --git a/pytest_pyodide/utils.py b/pytest_pyodide/utils.py index 7da9bcb8..56e6436e 100644 --- a/pytest_pyodide/utils.py +++ b/pytest_pyodide/utils.py @@ -6,6 +6,8 @@ import pytest +from .hook import RUNTIMES + @contextlib.contextmanager def set_webdriver_script_timeout(selenium, script_timeout: float | None): @@ -47,7 +49,7 @@ def maybe_skip_test(item, dist_dir, delayed=False): loading the selenium_standalone fixture which takes a long time. """ - browsers = "|".join(["firefox", "chrome", "node"]) + browsers = "|".join(RUNTIMES) skip_msg = None # Testing a package. Skip the test if the package is not built. From 879374bbf28736d436af5cafdb508400d88e1620 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Tue, 2 Aug 2022 07:57:15 +0000 Subject: [PATCH 5/9] Install only required browsers --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aa3dd4db..15bc76e2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -87,8 +87,9 @@ jobs: if: ${{ matrix.test-config.runner == 'playwright' }} run: | pip install playwright==${{ matrix.test-config.playwright-version }} - # TODO: install only browsers that are required - python -m playwright install --with-deps + RUNTIME=$${{ matrix.test-config.runtime }} + RUNTIME="${RUNTIME//safari/webkit}" + python -m playwright install "${RUNTIME}" --with-deps - name: Install firefox uses: browser-actions/setup-firefox@latest From 492e65b1faaf61ee958c66b8ef2218e217948e64 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Wed, 3 Aug 2022 04:48:08 +0000 Subject: [PATCH 6/9] Fix replace syntax --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 15bc76e2..8f517660 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -87,8 +87,8 @@ jobs: if: ${{ matrix.test-config.runner == 'playwright' }} run: | pip install playwright==${{ matrix.test-config.playwright-version }} - RUNTIME=$${{ matrix.test-config.runtime }} - RUNTIME="${RUNTIME//safari/webkit}" + RUNTIME=${{ matrix.test-config.runtime }} + RUNTIME=${RUNTIME//safari/webkit} python -m playwright install "${RUNTIME}" --with-deps - name: Install firefox From 3f80ef07ece64d7a4c51554639321a940f4dd1da Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Wed, 3 Aug 2022 04:51:58 +0000 Subject: [PATCH 7/9] Add constants.py and move RUNTIMES variable into it --- pytest_pyodide/constants.py | 1 + pytest_pyodide/hook.py | 3 +-- pytest_pyodide/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 pytest_pyodide/constants.py diff --git a/pytest_pyodide/constants.py b/pytest_pyodide/constants.py new file mode 100644 index 00000000..81ad8479 --- /dev/null +++ b/pytest_pyodide/constants.py @@ -0,0 +1 @@ +RUNTIMES = ["firefox", "chrome", "node", "safari"] diff --git a/pytest_pyodide/hook.py b/pytest_pyodide/hook.py index 8b79635a..7c13a706 100644 --- a/pytest_pyodide/hook.py +++ b/pytest_pyodide/hook.py @@ -10,10 +10,9 @@ pytest_pycollect_makemodule as orig_pytest_pycollect_makemodule, ) +from .constants import RUNTIMES from .utils import parse_xfail_browsers -RUNTIMES = ["firefox", "chrome", "node", "safari"] - def pytest_configure(config): diff --git a/pytest_pyodide/utils.py b/pytest_pyodide/utils.py index 56e6436e..2b9e1e1f 100644 --- a/pytest_pyodide/utils.py +++ b/pytest_pyodide/utils.py @@ -6,7 +6,7 @@ import pytest -from .hook import RUNTIMES +from .constants import RUNTIMES @contextlib.contextmanager From b09fc52517aa278dd6c5eb3a7a69c3eb2ab450b5 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Wed, 3 Aug 2022 05:00:04 +0000 Subject: [PATCH 8/9] Fix runtime name --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f517660..8291ca04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,6 +89,8 @@ jobs: pip install playwright==${{ matrix.test-config.playwright-version }} RUNTIME=${{ matrix.test-config.runtime }} RUNTIME=${RUNTIME//safari/webkit} + RUNTIME=${RUNTIME//chrome/chromium} + python -m playwright install "${RUNTIME}" --with-deps - name: Install firefox From eb15f5b53c0eadbabded8828ae74f532e095332a Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Wed, 3 Aug 2022 05:00:28 +0000 Subject: [PATCH 9/9] Fix xfail test --- tests/test_marker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_marker.py b/tests/test_marker.py index 81544d8f..017f38d6 100644 --- a/tests/test_marker.py +++ b/tests/test_marker.py @@ -4,7 +4,10 @@ @pytest.mark.xfail_browsers( - node="Should xfail", firefox="Should xfail", chrome="Should xfail" + node="Should xfail", + firefox="Should xfail", + chrome="Should xfail", + safari="Should xfail", ) @run_in_pyodide def test_xfail_browser(selenium):