diff --git a/.circleci/config.yml b/.circleci/config.yml index 4c8ec1edf..50839643a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 defaults: &defaults working_directory: ~/repo docker: - - image: pyodide/pyodide-env:18 + - image: pyodide/pyodide-env:19 environment: - EMSDK_NUM_CORES: 3 EMCC_CORES: 3 @@ -26,11 +26,6 @@ jobs: steps: - checkout - - run: - name: Install prerequisites - command: | - pip install -r docs/requirements-doc.txt - - run: name: Test docs command: | @@ -166,7 +161,7 @@ jobs: - run: name: stack-size command: | - pytest -s benchmark/stack_usage.py | sed -n 's/## //pg' + pytest -s benchmark/stack_usage.py | sed -n 's/## //pg' || true test-emsdk: <<: *defaults @@ -188,11 +183,10 @@ jobs: name: test command: | mkdir test-results - pip install ruamel.yaml pytest \ --junitxml=test-results/junit.xml \ --verbose \ - -k 'not (chrome or firefox)' \ + -k 'not (chrome or firefox or node)' \ --cov=pyodide_build --cov=pyodide \ src pyodide-build packages/micropip/ @@ -313,6 +307,11 @@ workflows: test-params: -k firefox src packages/micropip requires: - build-core + - test-main: + name: test-core-node + test-params: -k node src packages/micropip + requires: + - build-core - test-main: name: test-packages-chrome test-params: -k chrome packages/test* packages/*/test* @@ -323,6 +322,11 @@ workflows: test-params: -k firefox packages/test* packages/*/test* requires: - build-packages + - test-main: + name: test-packages-node + test-params: -k "node and not numpy" packages/test* packages/*/test* + requires: + - build-packages - test-emsdk: requires: - build-core diff --git a/Dockerfile b/Dockerfile index 82f05619c..1f1701344 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,34 +12,10 @@ RUN apt-get update \ libgconf-2-4 "chromium=90.*" \ && rm -rf /var/lib/apt/lists/* -RUN pip3 --no-cache-dir install \ - black \ - "cython<3.0" \ - packaging \ - flake8 \ - hypothesis \ - "mypy==0.812" \ - pytest \ - pytest-asyncio \ - pytest-cov \ - pytest-httpserver \ - pytest-instafail \ - pytest-rerunfailures \ - pytest-xdist \ - pyyaml \ - "selenium==4.0.0.b3" \ - # Docs requirements - sphinx \ - sphinx_book_theme \ - myst-parser==0.13.3 \ - sphinxcontrib-napoleon \ - packaging \ - sphinx-js==3.1 \ - autodocsumm \ - docutils==0.16 \ - sphinx-argparse-cli~=1.6.0 \ - sphinx-version-warning~=1.1.2 \ - sphinx-issues +ADD docs/requirements-doc.txt requirements.txt / + +RUN pip3 --no-cache-dir install -r /requirements.txt \ + && pip3 --no-cache-dir install -r /requirements-doc.txt # Get firefox 70.0.1 and geckodriver RUN wget -qO- https://ftp.mozilla.org/pub/firefox/releases/87.0/linux-x86_64/en-US/firefox-87.0.tar.bz2 | tar jx \ diff --git a/conftest.py b/conftest.py index 2496c75a8..80f794289 100644 --- a/conftest.py +++ b/conftest.py @@ -3,12 +3,14 @@ Various common utilities for testing. """ import contextlib +import json import multiprocessing import textwrap import tempfile import time import os import pathlib +import pexpect import queue import sys import shutil @@ -78,29 +80,24 @@ class SeleniumWrapper: server_port, server_hostname="127.0.0.1", server_log=None, - build_dir=None, load_pyodide=True, script_timeout=20, ): - if build_dir is None: - build_dir = BUILD_PATH - - self.driver = self.get_driver() 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 - - if not (pathlib.Path(build_dir) / "test.html").exists(): - # selenium does not expose HTTP response codes - raise ValueError( - f"{(build_dir / 'test.html').resolve()} " f"does not exist!" - ) - self.driver.get(f"http://{server_hostname}:{server_port}/test.html") + 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.run_js( """ - window.pyodide = await loadPyodide({ indexURL : './', fullStdLib: false }); + let pyodide = await loadPyodide({ indexURL : './', fullStdLib: false, jsglobals : self }); + self.pyodide = pyodide; + globalThis.pyodide = pyodide; pyodide.globals.get; pyodide.pyodide_py.eval_code; pyodide.pyodide_py.eval_code_async; @@ -108,88 +105,41 @@ class SeleniumWrapper: pyodide.pyodide_py.unregister_js_module; pyodide.pyodide_py.find_imports; pyodide.runPython(""); - """ + """, ) self.save_state() self.restore_state() - self.script_timeout = script_timeout - self.driver.set_script_timeout(script_timeout) + + SETUP_CODE = pathlib.Path(ROOT_PATH / "tools/testsetup.js").read_text() + + def prepare_driver(self): + self.driver.get(f"{self.base_url}/test.html") + + 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 javascript_setup(self): - self.run_js("Error.stackTraceLimit = Infinity;", pyodide_checks=False) self.run_js( - """ - window.assert = function(cb, message=""){ - if(message !== ""){ - message = "\\n" + message; - } - if(cb() !== true){ - throw new Error(`Assertion failed: ${cb.toString().slice(6)}${message}`); - } - }; - window.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}` - ); - } - } - window.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); - }; - window.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); - }; - """, + SeleniumWrapper.SETUP_CODE, pyodide_checks=False, ) @property def logs(self): - logs = self.driver.execute_script("return window.logs;") + 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.driver.execute_script("window.logs = []") + self.run_js("self.logs = []", pyodide_checks=False) def run(self, code): return self.run_js( @@ -236,7 +186,7 @@ class SeleniumWrapper: try { pyodide._module._pythonexc2js(); } catch(e){ - console.error(`Python exited with error flag set! Error was:\n{e.message}`); + 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!`); @@ -245,7 +195,9 @@ class SeleniumWrapper: """ else: check_code = "" + return self.run_js_inner(code, check_code) + def run_js_inner(self, code, check_code): wrapper = """ let cb = arguments[arguments.length - 1]; let run = async () => { %s } @@ -259,9 +211,7 @@ class SeleniumWrapper: } })() """ - retval = self.driver.execute_async_script(wrapper % (code, check_code)) - if retval[0] == 0: return retval[1] else: @@ -350,6 +300,84 @@ class ChromeWrapper(SeleniumWrapper): options.add_argument("--js-flags=--expose-gc") return Chrome(options=options) + def collect_garbage(self): + self.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {}) + + +class NodeWrapper(SeleniumWrapper): + browser = "node" + + def init_node(self): + os.chdir("build") + self.p = pexpect.spawn( + f"node --expose-gc ../tools/node_test_driver.js {self.base_url}", timeout=60 + ) + self.p.setecho(False) + self.p.delaybeforesend = None + os.chdir("..") + + 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 () => { %s })(); + %s + return result; + """ % ( + 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(f"[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()) + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): @@ -388,12 +416,17 @@ def test_wrapper_check_for_memory_leaks(selenium, trace_hiwire_refs, trace_pypro selenium.enable_pyproxy_tracing() init_num_proxies = selenium.get_num_proxies() a = yield - selenium.disable_pyproxy_tracing() - selenium.restore_state() - # if there was an error in the body of the test, flush it out by calling - # get_result (we don't want to override the error message by raising a - # different error here.) - a.get_result() + try: + # If these guys cause a crash because the test really screwed things up, + # we override the error message with the better message returned by + # a.result() in the finally block. + selenium.disable_pyproxy_tracing() + selenium.restore_state() + finally: + # if there was an error in the body of the test, flush it out by calling + # get_result (we don't want to override the error message by raising a + # different error here.) + a.get_result() if trace_pyproxies and trace_hiwire_refs: delta_proxies = selenium.get_num_proxies() - init_num_proxies delta_keys = selenium.get_num_hiwire_keys() - init_num_keys @@ -410,10 +443,11 @@ def selenium_common(request, web_server_main, load_pyodide=True): cls = FirefoxWrapper elif request.param == "chrome": cls = ChromeWrapper + elif request.param == "node": + cls = NodeWrapper else: assert False selenium = cls( - build_dir=request.config.option.build_dir, server_port=server_port, server_hostname=server_hostname, server_log=server_log, @@ -422,10 +456,10 @@ def selenium_common(request, web_server_main, load_pyodide=True): try: yield selenium finally: - selenium.driver.quit() + selenium.quit() -@pytest.fixture(params=["firefox", "chrome"], scope="function") +@pytest.fixture(params=["firefox", "chrome", "node"], scope="function") def selenium_standalone(request, web_server_main): with selenium_common(request, web_server_main) as selenium: with set_webdriver_script_timeout( @@ -450,7 +484,7 @@ def selenium_webworker_standalone(request, web_server_main): # selenium instance cached at the module level -@pytest.fixture(params=["firefox", "chrome"], scope="module") +@pytest.fixture(params=["firefox", "chrome", "node"], scope="module") def selenium_module_scope(request, web_server_main): with selenium_common(request, web_server_main) as selenium: yield selenium diff --git a/docs/requirements-doc.txt b/docs/requirements-doc.txt index 64a588f15..75326bb99 100644 --- a/docs/requirements-doc.txt +++ b/docs/requirements-doc.txt @@ -1,11 +1,11 @@ -sphinx -sphinx_book_theme -myst-parser==0.13.3 -sphinxcontrib-napoleon -packaging # required by micropip -sphinx-js==3.1 autodocsumm docutils==0.16 +myst-parser==0.13.3 +packaging # required by micropip at import time +sphinx sphinx-argparse-cli~=1.6.0 -sphinx-version-warning~=1.1.2 +sphinx_book_theme +sphinxcontrib-napoleon sphinx-issues +sphinx-js==3.1 +sphinx-version-warning~=1.1.2 diff --git a/packages/matplotlib/test_matplotlib.py b/packages/matplotlib/test_matplotlib.py index 9006ba830..d4a097809 100644 --- a/packages/matplotlib/test_matplotlib.py +++ b/packages/matplotlib/test_matplotlib.py @@ -13,9 +13,9 @@ def get_backend(selenium_standalone): selenium = selenium_standalone return selenium.run( """ - import matplotlib - matplotlib.get_backend() - """ + import matplotlib + matplotlib.get_backend() + """ ) @@ -58,8 +58,9 @@ def check_comparison(selenium, prefix, num_fonts): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_matplotlib(selenium_standalone): - selenium = selenium_standalone +def test_matplotlib(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") selenium.run( """ @@ -74,6 +75,8 @@ def test_matplotlib(selenium_standalone): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check def test_svg(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") selenium.run("from matplotlib import pyplot as plt") selenium.run("plt.figure(); pass") @@ -88,6 +91,8 @@ def test_svg(selenium): @pytest.mark.skip_pyproxy_check def test_pdf(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") selenium.run("from matplotlib import pyplot as plt") selenium.run("plt.figure(); pass") @@ -131,8 +136,9 @@ def test_font_manager(selenium): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_rendering(selenium_standalone): - selenium = selenium_standalone +def test_rendering(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") if get_backend(selenium) == "module://matplotlib.backends.wasm_backend": pytest.skip( @@ -164,8 +170,9 @@ def test_rendering(selenium_standalone): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_draw_image(selenium_standalone): - selenium = selenium_standalone +def test_draw_image(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") if get_backend(selenium) == "module://matplotlib.backends.wasm_backend": pytest.skip( @@ -205,8 +212,9 @@ def test_draw_image(selenium_standalone): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_draw_image_affine_transform(selenium_standalone): - selenium = selenium_standalone +def test_draw_image_affine_transform(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") if get_backend(selenium) == "module://matplotlib.backends.wasm_backend": pytest.skip( @@ -276,8 +284,9 @@ def test_draw_image_affine_transform(selenium_standalone): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_draw_text_rotated(selenium_standalone): - selenium = selenium_standalone +def test_draw_text_rotated(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") if selenium.browser == "chrome": pytest.xfail(f"high recursion limit not supported for {selenium.browser}") selenium.load_package("matplotlib") @@ -331,8 +340,9 @@ def test_draw_text_rotated(selenium_standalone): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_draw_math_text(selenium_standalone): - selenium = selenium_standalone +def test_draw_math_text(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") if selenium.browser == "chrome": pytest.xfail(f"high recursion limit not supported for {selenium.browser}") selenium.load_package("matplotlib") @@ -454,8 +464,9 @@ def test_draw_math_text(selenium_standalone): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_custom_font_text(selenium_standalone): - selenium = selenium_standalone +def test_custom_font_text(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") if get_backend(selenium) == "module://matplotlib.backends.wasm_backend": pytest.skip( @@ -466,22 +477,22 @@ def test_custom_font_text(selenium_standalone): try: selenium.run( """ - from js import window - window.testing = True - import matplotlib.pyplot as plt - import numpy as np + from js import window + window.testing = True + import matplotlib.pyplot as plt + import numpy as np - f = {'fontname': 'cmsy10'} + f = {'fontname': 'cmsy10'} - t = np.arange(0.0, 2.0, 0.01) - s = 1 + np.sin(2 * np.pi * t) - plt.figure() - plt.title('A simple Sine Curve', **f) - plt.plot(t, s, linewidth=1.0, marker=11) - plt.plot(t, t) - plt.grid(True) - plt.show() - """ + t = np.arange(0.0, 2.0, 0.01) + s = 1 + np.sin(2 * np.pi * t) + plt.figure() + plt.title('A simple Sine Curve', **f) + plt.plot(t, s, linewidth=1.0, marker=11) + plt.plot(t, t) + plt.grid(True) + plt.show() + """ ) check_comparison(selenium, "canvas-custom-font-text", 2) @@ -491,8 +502,9 @@ def test_custom_font_text(selenium_standalone): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_zoom_on_polar_plot(selenium_standalone): - selenium = selenium_standalone +def test_zoom_on_polar_plot(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") if get_backend(selenium) == "module://matplotlib.backends.wasm_backend": pytest.skip( @@ -503,30 +515,30 @@ def test_zoom_on_polar_plot(selenium_standalone): try: selenium.run( """ - from js import window - window.testing = True + from js import window + window.testing = True - import numpy as np - import matplotlib.pyplot as plt - np.random.seed(42) + import numpy as np + import matplotlib.pyplot as plt + np.random.seed(42) - # Compute pie slices - N = 20 - theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False) - radii = 10 * np.random.rand(N) - width = np.pi / 4 * np.random.rand(N) + # Compute pie slices + N = 20 + theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False) + radii = 10 * np.random.rand(N) + width = np.pi / 4 * np.random.rand(N) - ax = plt.subplot(111, projection='polar') - bars = ax.bar(theta, radii, width=width, bottom=0.0) + ax = plt.subplot(111, projection='polar') + bars = ax.bar(theta, radii, width=width, bottom=0.0) - # Use custom colors and opacity - for r, bar in zip(radii, bars): - bar.set_facecolor(plt.cm.viridis(r / 10.)) - bar.set_alpha(0.5) + # Use custom colors and opacity + for r, bar in zip(radii, bars): + bar.set_facecolor(plt.cm.viridis(r / 10.)) + bar.set_alpha(0.5) - ax.set_rlim([0,5]) - plt.show() - """ + ax.set_rlim([0,5]) + plt.show() + """ ) check_comparison(selenium, "canvas-polar-zoom", 1) @@ -536,8 +548,9 @@ def test_zoom_on_polar_plot(selenium_standalone): @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check -def test_transparency(selenium_standalone): - selenium = selenium_standalone +def test_transparency(selenium): + if selenium.browser == "node": + pytest.xfail("No supported matplotlib backends on node") selenium.load_package("matplotlib") if get_backend(selenium) == "module://matplotlib.backends.wasm_backend": pytest.skip( diff --git a/packages/numpy/test_numpy.py b/packages/numpy/test_numpy.py index 394466d1f..880da8d29 100644 --- a/packages/numpy/test_numpy.py +++ b/packages/numpy/test_numpy.py @@ -12,7 +12,7 @@ def test_numpy(selenium): selenium.run_js( """ let xpy = pyodide.runPython('x'); - window.x = xpy.toJs(); + self.x = xpy.toJs(); xpy.destroy(); """ ) @@ -37,7 +37,7 @@ def test_typed_arrays(selenium): ("Float32Array", "float32"), ("Float64Array", "float64"), ): - selenium.run_js(f"window.array = new {jstype}([1, 2, 3, 4]);\n") + selenium.run_js(f"self.array = new {jstype}([1, 2, 3, 4]);\n") assert selenium.run( "from js import array\n" "npyarray = numpy.asarray(array.to_py())\n" @@ -264,7 +264,7 @@ def test_get_buffer_roundtrip(selenium, arg): import numpy as np x = {arg} `); - window.x_js_buf = pyodide.globals.get("x").getBuffer(); + self.x_js_buf = pyodide.globals.get("x").getBuffer(); x_js_buf.length = x_js_buf.data.length; """ ) @@ -303,7 +303,7 @@ def test_get_buffer_big_endian(selenium): selenium.run_js( """ await pyodide.loadPackage(['numpy']); - window.a = pyodide.runPython(` + self.a = pyodide.runPython(` import numpy as np np.arange(24, dtype="int16").byteswap().newbyteorder() `); diff --git a/packages/pandas/test_pandas.py b/packages/pandas/test_pandas.py index b14f445d8..3df8523c3 100644 --- a/packages/pandas/test_pandas.py +++ b/packages/pandas/test_pandas.py @@ -55,6 +55,8 @@ def test_load_largish_file(selenium_standalone, request, httpserver): pytest.xfail( "test_load_largish_file triggers a fatal runtime error in Chrome 89 see #1495" ) + if selenium.browser == "node": + pytest.xfail("open_url doesn't work in node") selenium.load_package("pandas") selenium.load_package("matplotlib") diff --git a/packages/pillow/test_pillow.py b/packages/pillow/test_pillow.py index 82c2a15d5..c57838f89 100644 --- a/packages/pillow/test_pillow.py +++ b/packages/pillow/test_pillow.py @@ -2,7 +2,8 @@ from pyodide_build.testing import run_in_pyodide @run_in_pyodide( - packages=["pillow"], xfail_browsers={"firefox": "timeout", "chrome": ""} + packages=["pillow"], + xfail_browsers={"firefox": "timeout", "chrome": "", "node": "timeout"}, ) def test_pillow(): from PIL import Image, ImageDraw, ImageOps diff --git a/packages/test_common.py b/packages/test_common.py index 1a6d58ee0..8b3c15519 100644 --- a/packages/test_common.py +++ b/packages/test_common.py @@ -29,6 +29,7 @@ def registered_packages_meta(): UNSUPPORTED_PACKAGES: dict = { "chrome": ["scikit-image", "statsmodels"], "firefox": [], + "node": ["scikit-image", "statsmodels"], } diff --git a/pyodide-build/pyodide_build/testing.py b/pyodide-build/pyodide_build/testing.py index b8ad64072..4b983828f 100644 --- a/pyodide-build/pyodide_build/testing.py +++ b/pyodide-build/pyodide_build/testing.py @@ -2,6 +2,7 @@ import pytest import inspect from typing import Callable, Dict, List, Optional, Union import contextlib +from base64 import b64encode def _run_in_pyodide_get_source(f): @@ -17,6 +18,13 @@ def _run_in_pyodide_get_source(f): return "".join(lines) +def chunkstring(string, length): + return (string[0 + i : length + i] for i in range(0, len(string), length)) + + +from pprint import pformat + + def run_in_pyodide( _function: Optional[Callable] = None, *, @@ -64,13 +72,17 @@ def run_in_pyodide( await_kw = "" source = _run_in_pyodide_get_source(f) filename = inspect.getsourcefile(f) + encoded = pformat( + list(chunkstring(b64encode(source.encode()).decode(), 100)) + ) + selenium.run_js( f""" let eval_code = pyodide.pyodide_py.eval_code; try {{ eval_code.callKwargs( {{ - source : {source!r}, + source : atob({encoded}.join("")), globals : pyodide._module.globals, filename : {filename!r} }} @@ -125,11 +137,11 @@ def set_webdriver_script_timeout(selenium, script_timeout: Optional[Union[int, f value of the timeout in seconds """ if script_timeout is not None: - selenium.driver.set_script_timeout(script_timeout) + selenium.set_script_timeout(script_timeout) yield # revert to the initial value if script_timeout is not None: - selenium.driver.set_script_timeout(selenium.script_timeout) + selenium.set_script_timeout(selenium.script_timeout) def parse_driver_timeout(request) -> Optional[Union[int, float]]: diff --git a/pyodide-build/pyodide_build/tests/test_testing.py b/pyodide-build/pyodide_build/tests/test_testing.py index 4401653f2..7bf9de06b 100644 --- a/pyodide-build/pyodide_build/tests/test_testing.py +++ b/pyodide-build/pyodide_build/tests/test_testing.py @@ -1,19 +1,16 @@ from pyodide_build.testing import set_webdriver_script_timeout -class _MockDriver: - def set_script_timeout(self, value): - self._timeout = value - - class _MockSelenium: script_timeout = 2 - driver = _MockDriver() + + def set_script_timeout(self, value): + self._timeout = value def test_set_webdriver_script_timeout(): selenium = _MockSelenium() - assert not hasattr(selenium.driver, "_timeout") + assert not hasattr(selenium, "_timeout") with set_webdriver_script_timeout(selenium, script_timeout=10): - assert selenium.driver._timeout == 10 - assert selenium.driver._timeout == 2 + assert selenium._timeout == 10 + assert selenium._timeout == 2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..cb72bacde --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ + # core + cython<3.0 + packaging + pyyaml + ruamel.yaml + # lint + black + flake8 + mypy==0.812 + # testing + hypothesis + pexpect + pytest + pytest-asyncio + pytest-cov + pytest-httpserver + pytest-instafail + pytest-rerunfailures + pytest-xdist + selenium==4.0.0.b3 diff --git a/run_docker b/run_docker index f939c3753..e4db7d511 100755 --- a/run_docker +++ b/run_docker @@ -1,7 +1,7 @@ #!/usr/bin/env bash PYODIDE_IMAGE_REPO="pyodide" -PYODIDE_IMAGE_TAG="18" +PYODIDE_IMAGE_TAG="19" PYODIDE_PREBUILT_IMAGE_TAG="0.17.0" DEFAULT_PYODIDE_DOCKER_IMAGE="${PYODIDE_IMAGE_REPO}/pyodide-env:${PYODIDE_IMAGE_TAG}" DEFAULT_PYODIDE_SYSTEM_PORT="8000" diff --git a/src/js/load-pyodide.js b/src/js/load-pyodide.js index 61d717767..61b691851 100644 --- a/src/js/load-pyodide.js +++ b/src/js/load-pyodide.js @@ -52,7 +52,22 @@ if (globalThis.document) { globalThis.importScripts(url); }; } else if (typeof process !== "undefined" && process.release.name === "node") { - loadScript = async (url) => import(path.resolve(url)); + const pathPromise = import("path").then((M) => M.default); + const fetchPromise = import("node-fetch").then((M) => M.default); + const vmPromise = import("vm").then((M) => M.default); + loadScript = async (url) => { + if (url.includes("://")) { + // If it's a url, have to load it with fetch and then eval it. + const fetch = await fetchPromise; + const vm = await vmPromise; + vm.runInThisContext(await (await fetch(url)).text()); + } else { + // Otherwise, hopefully it is a relative path we can load from the file + // system. + const path = await pathPromise; + await import(path.resolve(url)); + } + }; } else { throw new Error("Cannot determine runtime environment"); } diff --git a/src/js/package-lock.json b/src/js/package-lock.json index 8c859656e..560f2943f 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -288,6 +288,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", diff --git a/src/js/package.json b/src/js/package.json index 8cc046925..c169f2c72 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -28,6 +28,7 @@ } }, "dependencies": { + "base-64": "^1.0.0", "node-fetch": "^2.6.1" } } diff --git a/src/js/pyodide.js b/src/js/pyodide.js index ca2ab37b8..5e38c7539 100644 --- a/src/js/pyodide.js +++ b/src/js/pyodide.js @@ -173,7 +173,7 @@ function fixRecursionLimit() { * @async */ export async function loadPyodide(config) { - const default_config = { fullStdLib: true }; + const default_config = { fullStdLib: true, jsglobals: globalThis }; config = Object.assign(default_config, config); if (globalThis.__pyodide_module) { if (globalThis.languagePluginURL) { @@ -243,7 +243,7 @@ def temp(pyodide_js, Module, jsglobals): print("Python initialization complete") `); - Module.init_dict.get("temp")(pyodide, Module, globalThis); + Module.init_dict.get("temp")(pyodide, Module, config.jsglobals); // Module.runPython works starting from here! // Wrap "globals" in a special Proxy that allows `pyodide.globals.x` access. diff --git a/src/templates/test.html b/src/templates/test.html index b7667971e..09915ecfa 100644 --- a/src/templates/test.html +++ b/src/templates/test.html @@ -1,11 +1,8 @@ +
- diff --git a/src/tests/test_asyncio.py b/src/tests/test_asyncio.py index 61434d93b..e52a7ae8d 100644 --- a/src/tests/test_asyncio.py +++ b/src/tests/test_asyncio.py @@ -140,7 +140,7 @@ def test_then_jsproxy(selenium): selenium.run( """ p = Promise.new(create_once_callable(prom)) - p.finally_(onfinally) + p.finally_(onfinally).catch(onrejected) # node gets angry if we don't catch it! reject(10) """ ) @@ -149,6 +149,8 @@ def test_then_jsproxy(selenium): """ assert finally_occurred finally_occurred = False + assert err == 10 + err = None """ ) @@ -189,11 +191,11 @@ def test_await_error(selenium): async function async_js_raises(){ throw new Error("This is an error message!"); } - window.async_js_raises = async_js_raises; + self.async_js_raises = async_js_raises; function js_raises(){ throw new Error("This is an error message!"); } - window.js_raises = js_raises; + self.js_raises = js_raises; pyodide.runPython(` from js import async_js_raises, js_raises async def test(): @@ -309,7 +311,7 @@ def test_eval_code_await_error(selenium): console.log("Hello there???"); throw new Error("This is an error message!"); } - window.async_js_raises = async_js_raises; + self.async_js_raises = async_js_raises; pyodide.runPython(` from js import async_js_raises from pyodide import eval_code_async @@ -336,7 +338,7 @@ def test_eval_code_await_error(selenium): def test_ensure_future_memleak(selenium): selenium.run_js( """ - window.o = { "xxx" : 777 }; + self.o = { "xxx" : 777 }; pyodide.runPython(` import asyncio from js import o diff --git a/src/tests/test_console.py b/src/tests/test_console.py index 019e3bfd0..081bb3996 100644 --- a/src/tests/test_console.py +++ b/src/tests/test_console.py @@ -1,5 +1,6 @@ import pytest from pathlib import Path +import time import sys from conftest import selenium_common @@ -293,8 +294,12 @@ def test_interactive_console_top_level_await(selenium, safe_selenium_sys_redirec """ ) selenium.run("shell.push('from js import fetch')") - selenium.run("shell.push('await (await fetch(`packages.json`)).json()')") - assert selenium.run("result") == None + time.sleep(0.2) + selenium.run("""shell.push("await (await fetch('packages.json')).json()")""") + time.sleep(0.2) + res = selenium.run("result") + assert isinstance(res, dict) + assert res["dependencies"]["micropip"] == ["pyparsing", "packaging", "distutils"] @pytest.fixture(params=["firefox", "chrome"], scope="function") diff --git a/src/tests/test_core_python.py b/src/tests/test_core_python.py index 137632c40..7431a87ea 100644 --- a/src/tests/test_core_python.py +++ b/src/tests/test_core_python.py @@ -10,7 +10,7 @@ def test_cpython_core(python_test, selenium, request): name, error_flags = python_test # keep only flags related to the current browser - flags_to_remove = ["firefox", "chrome"] + flags_to_remove = ["firefox", "chrome", "node"] flags_to_remove.remove(selenium.browser) for flag in flags_to_remove: if "crash-" + flag in error_flags: diff --git a/src/tests/test_filesystem.py b/src/tests/test_filesystem.py index 0dacebbf3..a886cfac6 100644 --- a/src/tests/test_filesystem.py +++ b/src/tests/test_filesystem.py @@ -10,11 +10,15 @@ import pytest def test_idbfs_persist_code(selenium_standalone): """can we persist files created by user python code?""" selenium = selenium_standalone + if selenium.browser == "node": + fstype = "NODEFS" + else: + fstype = "IDBFS" # create mount selenium.run_js( - """ + f""" pyodide.FS.mkdir('/lib/python3.9/site-packages/test_idbfs'); - pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, "/lib/python3.9/site-packages/test_idbfs") + pyodide.FS.mount(pyodide.FS.filesystems.{fstype}, {{root : "."}}, "/lib/python3.9/site-packages/test_idbfs"); """ ) # create file in mount @@ -41,15 +45,13 @@ def test_idbfs_persist_code(selenium_standalone): """ ) # refresh page and re-fixture - selenium.driver.refresh() - selenium.javascript_setup() + selenium.refresh() selenium.run_js( """ - window.pyodide = await loadPyodide({ indexURL : './', fullStdLib: false }); + self.pyodide = await loadPyodide({ indexURL : './', fullStdLib: false }); """ ) selenium.save_state() - selenium.restore_state() # idbfs isn't magically loaded selenium.run_js( """ @@ -67,9 +69,9 @@ def test_idbfs_persist_code(selenium_standalone): ) # re-mount selenium.run_js( - """ + f""" pyodide.FS.mkdir('/lib/python3.9/site-packages/test_idbfs'); - pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, "/lib/python3.9/site-packages/test_idbfs"); + pyodide.FS.mount(pyodide.FS.filesystems.{fstype}, {{root : "."}}, "/lib/python3.9/site-packages/test_idbfs"); """ ) # sync FROM idbfs @@ -92,3 +94,7 @@ def test_idbfs_persist_code(selenium_standalone): `); """ ) + # remove file + selenium.run_js( + """pyodide.FS.unlink("/lib/python3.9/site-packages/test_idbfs/__init__.py")""" + ) diff --git a/src/tests/test_jsproxy.py b/src/tests/test_jsproxy.py index 7f95fdb16..98d4604a3 100644 --- a/src/tests/test_jsproxy.py +++ b/src/tests/test_jsproxy.py @@ -6,8 +6,8 @@ from pyodide_build.testing import run_in_pyodide def test_jsproxy_dir(selenium): result = selenium.run_js( """ - window.a = { x : 2, y : "9" }; - window.b = function(){}; + self.a = { x : 2, y : "9" }; + self.b = function(){}; let pyresult = pyodide.runPython(` from js import a from js import b @@ -43,7 +43,7 @@ def test_jsproxy_dir(selenium): assert set1.isdisjoint(a_items) selenium.run_js( """ - window.a = [0,1,2,3,4,5,6,7,8,9]; + self.a = [0,1,2,3,4,5,6,7,8,9]; a[27] = 0; a[":"] = 0; a["/"] = 0; @@ -69,7 +69,7 @@ def test_jsproxy_getattr(selenium): assert ( selenium.run_js( """ - window.a = { x : 2, y : "9", typeof : 7 }; + self.a = { x : 2, y : "9", typeof : 7 }; let pyresult = pyodide.runPython(` from js import a [ a.x, a.y, a.typeof ] @@ -83,7 +83,9 @@ def test_jsproxy_getattr(selenium): ) -def test_jsproxy(selenium): +def test_jsproxy_document(selenium): + if selenium.browser == "node": + pytest.xfail("No document in node") selenium.run("from js import document") assert ( selenium.run( @@ -97,13 +99,50 @@ def test_jsproxy(selenium): ) assert selenium.run("document.body.children[0].tagName") == "DIV" assert selenium.run("repr(document)") == "[object HTMLDocument]" - - selenium.run_js("window.square = function (x) { return x*x; }") - assert selenium.run("from js import square\n" "square(2)") == 4 assert ( - selenium.run("from js import ImageData\n" "ImageData.new(64, 64).width") == 64 + selenium.run( + """ + from js import document + el = document.createElement('div') + len(dir(el)) >= 200 and 'appendChild' in dir(el) + """ + ) + is True ) - assert selenium.run("from js import ImageData\n" "ImageData.typeof") == "function" + assert ( + selenium.run( + """ + from js import ImageData + ImageData.new(64, 64).width + """ + ) + == 64 + ) + assert ( + selenium.run( + """ + from js import ImageData + ImageData.typeof + """ + ) + == "function" + ) + + +def test_jsproxy_function(selenium): + selenium.run_js("self.square = function (x) { return x*x; };") + assert ( + selenium.run( + """ + from js import square + square(2) + """ + ) + == 4 + ) + + +def test_jsproxy_class(selenium): selenium.run_js( """ class Point { @@ -112,7 +151,8 @@ def test_jsproxy(selenium): this.y = y; } } - window.TEST = new Point(42, 43);""" + self.TEST = new Point(42, 43); + """ ) assert ( selenium.run( @@ -124,9 +164,12 @@ def test_jsproxy(selenium): ) is False ) + + +def test_jsproxy_map(selenium): selenium.run_js( """ - window.TEST = new Map([["x", 42], ["y", 43]]); + self.TEST = new Map([["x", 42], ["y", 43]]); """ ) assert ( @@ -153,7 +196,7 @@ def test_jsproxy(selenium): ) selenium.run_js( """ - window.TEST = {foo: 'bar', baz: 'bap'} + self.TEST = {foo: 'bar', baz: 'bap'} """ ) assert ( @@ -165,16 +208,6 @@ def test_jsproxy(selenium): ) is True ) - assert ( - selenium.run( - """ - from js import document - el = document.createElement('div') - len(dir(el)) >= 200 and 'appendChild' in dir(el) - """ - ) - is True - ) def test_jsproxy_iter(selenium): @@ -190,7 +223,7 @@ def test_jsproxy_iter(selenium): } }; } - window.ITER = makeIterator([1, 2, 3]);""" + self.ITER = makeIterator([1, 2, 3]);""" ) assert selenium.run("from js import ITER\n" "list(ITER)") == [1, 2, 3] @@ -198,7 +231,7 @@ def test_jsproxy_iter(selenium): def test_jsproxy_implicit_iter(selenium): selenium.run_js( """ - window.ITER = [1, 2, 3]; + self.ITER = [1, 2, 3]; """ ) assert selenium.run("from js import ITER, Object\n" "list(ITER)") == [1, 2, 3] @@ -216,7 +249,7 @@ def test_jsproxy_call(selenium): assert ( selenium.run_js( """ - window.f = function(){ return arguments.length; }; + self.f = function(){ return arguments.length; }; let pyresult = pyodide.runPython( ` from js import f @@ -236,7 +269,7 @@ def test_jsproxy_call_kwargs(selenium): assert ( selenium.run_js( """ - window.kwarg_function = ({ a = 1, b = 1 }) => { + self.kwarg_function = ({ a = 1, b = 1 }) => { return [a, b]; }; return pyodide.runPython( @@ -255,7 +288,7 @@ def test_jsproxy_call_kwargs(selenium): def test_jsproxy_call_meth_py(selenium): assert selenium.run_js( """ - window.a = {}; + self.a = {}; return pyodide.runPython( ` from js import a @@ -272,7 +305,7 @@ def test_jsproxy_call_meth_py(selenium): def test_jsproxy_call_meth_js(selenium): assert selenium.run_js( """ - window.a = {}; + self.a = {}; function f(){return this;} a.f = f; return pyodide.runPython( @@ -288,7 +321,7 @@ def test_jsproxy_call_meth_js(selenium): def test_jsproxy_call_meth_js_kwargs(selenium): assert selenium.run_js( """ - window.a = {}; + self.a = {}; function f({ x = 1, y = 1 }){ return [this, x, y]; } @@ -327,23 +360,23 @@ def test_import_bind(): @run_in_pyodide def test_nested_attribute_access(): import js - from js import window + from js import self - js.URL.createObjectURL - window.URL.createObjectURL + assert js.Float64Array.BYTES_PER_ELEMENT == 8 + assert self.Float64Array.BYTES_PER_ELEMENT == 8 @run_in_pyodide def test_window_isnt_super_weird_anymore(): import js - from js import window, Array + from js import self, Array - assert window.Array != window - assert window.Array == Array - assert window.window.window.window == window - assert js.window.Array == Array - assert js.window.window.window.window == window - assert window.window.window.window.Array == Array + assert self.Array != self + assert self.Array == Array + assert self.self.self.self == self + assert js.self.Array == Array + assert js.self.self.self.self == self + assert self.self.self.self.Array == Array @pytest.mark.skip_refcount_check @@ -437,7 +470,7 @@ def test_nested_import(selenium_standalone): assert ( selenium.run_js( """ - window.a = { b : { c : { d : 2 } } }; + self.a = { b : { c : { d : 2 } } }; return pyodide.runPython("from js.a.b import c; c.d"); """ ) @@ -493,7 +526,7 @@ def test_register_jsmodule_docs_example(selenium_standalone): def test_object_entries_keys_values(selenium): selenium.run_js( """ - window.x = { a : 2, b : 3, c : 4 }; + self.x = { a : 2, b : 3, c : 4 }; pyodide.runPython(` from js import x assert x.object_entries().to_py() == [["a", 2], ["b", 3], ["c", 4]] @@ -551,7 +584,7 @@ def test_mixins_feature_presence(selenium): def test_mixins_calls(selenium): result = selenium.run_js( """ - window.testObjects = {}; + self.testObjects = {}; testObjects.iterable = { *[Symbol.iterator](){ yield 3; yield 5; yield 7; } }; @@ -604,8 +637,8 @@ def test_mixins_calls(selenium): def test_mixins_errors(selenium): selenium.run_js( """ - window.a = []; - window.b = { + self.a = []; + self.b = { has(){ return false; }, get(){ return undefined; }, set(){ return false; }, @@ -625,7 +658,7 @@ def test_mixins_errors(selenium): del b[0] `); - window.c = { + self.c = { next(){}, length : 1, get(){}, @@ -633,7 +666,7 @@ def test_mixins_errors(selenium): has(){}, then(){} }; - window.d = { + self.d = { [Symbol.iterator](){}, }; pyodide.runPython("from js import c, d"); @@ -670,8 +703,8 @@ def test_mixins_errors(selenium): await c `); - window.l = [0, false, NaN, undefined, null]; - window.l[6] = 7; + self.l = [0, false, NaN, undefined, null]; + self.l[6] = 7; await pyodide.runPythonAsync(` from unittest import TestCase raises = TestCase().assertRaises @@ -691,11 +724,11 @@ def test_mixins_errors(selenium): l[3]; l[4] `); - window.l = [0, false, NaN, undefined, null]; - window.l[6] = 7; - let a = Array.from(window.l.entries()); + self.l = [0, false, NaN, undefined, null]; + self.l[6] = 7; + let a = Array.from(self.l.entries()); a.splice(5, 1); - window.m = new Map(a); + self.m = new Map(a); await pyodide.runPythonAsync(` from js import m from unittest import TestCase @@ -791,7 +824,7 @@ def test_memory_leaks(selenium): # refcounts are tested automatically in conftest by default selenium.run_js( """ - window.a = [1,2,3]; + self.a = [1,2,3]; pyodide.runPython(` from js import a repr(a) diff --git a/src/tests/test_package_loading.py b/src/tests/test_package_loading.py index 13202c6f4..707acdf02 100644 --- a/src/tests/test_package_loading.py +++ b/src/tests/test_package_loading.py @@ -5,15 +5,17 @@ from pathlib import Path @pytest.mark.parametrize("active_server", ["main", "secondary"]) def test_load_from_url(selenium_standalone, web_server_secondary, active_server): - + selenium = selenium_standalone + if selenium.browser == "node": + pytest.xfail("Loading urls in node seems to time out right now") if active_server == "secondary": url, port, log_main = web_server_secondary - log_backup = selenium_standalone.server_log + log_backup = selenium.server_log elif active_server == "main": _, _, log_backup = web_server_secondary - log_main = selenium_standalone.server_log - url = selenium_standalone.server_hostname - port = selenium_standalone.server_port + log_main = selenium.server_log + url = selenium.server_hostname + port = selenium.server_port else: raise AssertionError() @@ -23,26 +25,26 @@ def test_load_from_url(selenium_standalone, web_server_secondary, active_server) fh_main.seek(0, 2) fh_backup.seek(0, 2) - selenium_standalone.load_package(f"http://{url}:{port}/pyparsing.js") - assert "Skipping unknown package" not in selenium_standalone.logs + selenium.load_package(f"http://{url}:{port}/pyparsing.js") + assert "Skipping unknown package" not in selenium.logs - # check that all ressources were loaded from the active server + # check that all resources were loaded from the active server txt = fh_main.read() assert '"GET /pyparsing.js HTTP/1.1" 200' in txt assert '"GET /pyparsing.data HTTP/1.1" 200' in txt - # no additional ressources were loaded from the other server + # no additional resources were loaded from the other server assert len(fh_backup.read()) == 0 - selenium_standalone.run( + selenium.run( """ from pyparsing import Word, alphas repr(Word(alphas).parseString('hello')) """ ) - selenium_standalone.load_package(f"http://{url}:{port}/pytz.js") - selenium_standalone.run("import pytz") + selenium.load_package(f"http://{url}:{port}/pytz.js") + selenium.run("import pytz") def test_load_relative_url(selenium_standalone): @@ -146,13 +148,13 @@ def test_load_package_unknown(selenium_standalone): shutil.copyfile(build_dir / "pyparsing.data", build_dir / "pyparsing-custom.data") try: - selenium_standalone.load_package(f"http://{url}:{port}/pyparsing-custom.js") + selenium_standalone.load_package(f"./pyparsing-custom.js") finally: (build_dir / "pyparsing-custom.js").unlink() (build_dir / "pyparsing-custom.data").unlink() assert selenium_standalone.run_js( - "return window.pyodide.loadedPackages.hasOwnProperty('pyparsing-custom')" + "return pyodide.loadedPackages.hasOwnProperty('pyparsing-custom')" ) diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py index d6f21015c..1c401097e 100644 --- a/src/tests/test_pyodide.py +++ b/src/tests/test_pyodide.py @@ -177,7 +177,6 @@ def test_hiwire_is_promise(selenium): "1", "'x'", "''", - "document.all", "false", "undefined", "null", @@ -202,6 +201,11 @@ def test_hiwire_is_promise(selenium): f"return pyodide._module.hiwire.isPromise({s}) === false;" ) + if not selenium.browser == "node": + assert selenium.run_js( + f"return pyodide._module.hiwire.isPromise(document.all) === false;" + ) + assert selenium.run_js( "return pyodide._module.hiwire.isPromise(Promise.resolve()) === true;" ) @@ -229,7 +233,7 @@ def test_keyboard_interrupt(selenium): """ x = new Int8Array(1) pyodide._module.setInterruptBuffer(x) - window.triggerKeyboardInterrupt = function(){ + self.triggerKeyboardInterrupt = function(){ x[0] = 2; } try { @@ -311,7 +315,7 @@ def test_run_python_js_error(selenium): function throwError(){ throw new Error("blah!"); } - window.throwError = throwError; + self.throwError = throwError; pyodide.runPython(` from js import throwError from unittest import TestCase @@ -327,7 +331,7 @@ def test_run_python_js_error(selenium): def test_create_once_callable(selenium): selenium.run_js( """ - window.call7 = function call7(f){ + self.call7 = function call7(f){ return f(7); } pyodide.runPython(` @@ -364,14 +368,14 @@ def test_create_once_callable(selenium): def test_create_proxy(selenium): selenium.run_js( """ - window.testAddListener = function(f){ - window.listener = f; + self.testAddListener = function(f){ + self.listener = f; } - window.testCallListener = function(f){ - return window.listener(); + self.testCallListener = function(f){ + return self.listener(); } - window.testRemoveListener = function(f){ - return window.listener === f; + self.testRemoveListener = function(f){ + return self.listener === f; } pyodide.runPython(` from pyodide import create_proxy @@ -428,7 +432,7 @@ def test_docstrings_b(selenium): sig_then_should_equal = "(onfulfilled, onrejected)" ds_once_should_equal = dedent_docstring(create_once_callable.__doc__) sig_once_should_equal = "(obj)" - selenium.run_js("window.a = Promise.resolve();") + selenium.run_js("self.a = Promise.resolve();") [ds_then, sig_then, ds_once, sig_once] = selenium.run( """ from js import a @@ -508,6 +512,10 @@ def test_fatal_error(selenium_standalone): def strip_stack_trace(x): x = re.sub("\n.*site-packages.*", "", x) x = re.sub("/lib/python.*/", "", x) + x = re.sub("/lib/python.*/", "", x) + x = re.sub("warning: no [bB]lob.*\n", "", x) + x = re.sub("Error: intentionally triggered fatal error!", "{}", x) + x = re.sub(" +at .*\n", "", x) return x assert ( @@ -515,18 +523,18 @@ def test_fatal_error(selenium_standalone): == dedent( strip_stack_trace( """ - Python initialization complete - Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers. - The cause of the fatal error was: - {} - Stack (most recent call first): - File "