From f0bd568a3165db34ca759475a3a2e883001bfdbd Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 20 Jul 2021 08:48:27 +0000 Subject: [PATCH] Set up pytest node tests (#1717) --- .circleci/config.yml | 22 +- Dockerfile | 32 +-- conftest.py | 218 ++++++++++-------- docs/requirements-doc.txt | 14 +- packages/matplotlib/test_matplotlib.py | 121 +++++----- packages/numpy/test_numpy.py | 8 +- packages/pandas/test_pandas.py | 2 + packages/pillow/test_pillow.py | 3 +- packages/test_common.py | 1 + pyodide-build/pyodide_build/testing.py | 18 +- .../pyodide_build/tests/test_testing.py | 15 +- requirements.txt | 20 ++ run_docker | 2 +- src/js/load-pyodide.js | 17 +- src/js/package-lock.json | 5 + src/js/package.json | 1 + src/js/pyodide.js | 4 +- src/templates/test.html | 6 +- src/tests/test_asyncio.py | 12 +- src/tests/test_console.py | 9 +- src/tests/test_core_python.py | 2 +- src/tests/test_filesystem.py | 22 +- src/tests/test_jsproxy.py | 141 ++++++----- src/tests/test_package_loading.py | 30 +-- src/tests/test_pyodide.py | 54 +++-- src/tests/test_pyproxy.py | 45 ++-- src/tests/test_python.py | 7 +- src/tests/test_typeconversions.py | 104 ++++----- src/tests/test_webloop.py | 8 +- tools/node_test_driver.js | 75 ++++++ tools/testsetup.js | 76 ++++++ 31 files changed, 695 insertions(+), 399 deletions(-) create mode 100644 requirements.txt create mode 100644 tools/node_test_driver.js create mode 100644 tools/testsetup.js 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 "", line 8 in h - File "", line 6 in g - File "", line 4 in f - File "", line 9 in - File "/lib/pythonxxx/site-packages/pyodide/_base.py", line 242 in run - File "/lib/pythonxxx/site-packages/pyodide/_base.py", line 344 in eval_code - """ + 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 "", line 8 in h + File "", line 6 in g + File "", line 4 in f + File "", line 9 in + File "/lib/pythonxxx/site-packages/pyodide/_base.py", line 242 in run + File "/lib/pythonxxx/site-packages/pyodide/_base.py", line 344 in eval_code + """ ) ).strip() ) diff --git a/src/tests/test_pyproxy.py b/src/tests/test_pyproxy.py index 16eca1845..9b20d10e9 100644 --- a/src/tests/test_pyproxy.py +++ b/src/tests/test_pyproxy.py @@ -1,5 +1,6 @@ # See also test_typeconversions, and test_python. import pytest +import time def test_pyproxy_class(selenium): @@ -12,7 +13,7 @@ def test_pyproxy_class(selenium): return value * 64 f = Foo() `); - window.f = pyodide.globals.get('f'); + self.f = pyodide.globals.get('f'); assert(() => f.type === "Foo"); let f_get_value = f.get_value assert(() => f_get_value(2) === 128); @@ -22,7 +23,7 @@ def test_pyproxy_class(selenium): f.baz = 32; assert(() => f.baz === 32); pyodide.runPython(`assert hasattr(f, 'baz')`) - window.f_props = Object.getOwnPropertyNames(f); + self.f_props = Object.getOwnPropertyNames(f); delete f.baz pyodide.runPython(`assert not hasattr(f, 'baz')`) assert(() => f.toString().startsWith(" { window.val = val; }); + self.x = new FinalizationRegistry((val) => { self.val = val; }); x.register({}, 77); gc(); """ ) - assert selenium.run_js("return window.val;") == 77 + time.sleep(0.1) + selenium.run_js( + """ + gc(); + """ + ) + assert selenium.run_js("return self.val;") == 77 selenium.run_js( """ - window.res = new Map(); + self.res = new Map(); let d = pyodide.runPython(` from js import res @@ -579,7 +586,7 @@ def test_pyproxy_gc(selenium): d.destroy() """ ) - selenium.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {}) + selenium.collect_garbage() selenium.run( """ @@ -587,19 +594,19 @@ def test_pyproxy_gc(selenium): del d """ ) - selenium.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {}) + selenium.collect_garbage() a = selenium.run_js("return Array.from(res.entries());") assert dict(a) == {0: 2, 1: 3, 2: 4, 3: 2, "destructor_ran": True} @pytest.mark.skip_pyproxy_check def test_pyproxy_gc_destroy(selenium): - if selenium.browser != "chrome": + if not hasattr(selenium, "collect_garbage"): pytest.skip("No gc exposed") selenium.run_js( """ - window.res = new Map(); + self.res = new Map(); let d = pyodide.runPython(` from js import res def get_ref_count(x): @@ -627,7 +634,7 @@ def test_pyproxy_gc_destroy(selenium): get_ref_count.destroy(); """ ) - selenium.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {}) + selenium.collect_garbage() selenium.run( """ get_ref_count(4) @@ -783,7 +790,7 @@ def test_pyproxy_call(selenium): def f(x=2, y=3): return to_js([x, y]) `); - window.f = pyodide.globals.get("f"); + self.f = pyodide.globals.get("f"); """ ) diff --git a/src/tests/test_python.py b/src/tests/test_python.py index f38a8a42d..c07ed94bd 100644 --- a/src/tests/test_python.py +++ b/src/tests/test_python.py @@ -3,10 +3,11 @@ import pytest def test_init(selenium_standalone): assert "Python initialization complete" in selenium_standalone.logs.splitlines() - assert len(selenium_standalone.driver.window_handles) == 1 def test_webbrowser(selenium): + if selenium.browser == "node": + pytest.xfail("Webbrowser doesn't work in node") selenium.run_async("import antigravity") assert len(selenium.driver.window_handles) == 2 @@ -17,6 +18,8 @@ def test_print(selenium): def test_import_js(selenium): + if selenium.browser == "node": + pytest.xfail("No window in node") result = selenium.run( """ import js @@ -47,6 +50,8 @@ def test_globals_get_multiple(selenium): def test_open_url(selenium, httpserver): + if selenium.browser == "node": + pytest.xfail("XMLHttpRequest not available in node") httpserver.expect_request("/data").respond_with_data( b"HELLO", content_type="text/text", headers={"Access-Control-Allow-Origin": "*"} ) diff --git a/src/tests/test_typeconversions.py b/src/tests/test_typeconversions.py index 76eac1be8..31a2d5990 100644 --- a/src/tests/test_typeconversions.py +++ b/src/tests/test_typeconversions.py @@ -7,14 +7,14 @@ from conftest import selenium_context_manager @given(s=text()) -@settings(deadline=600) +@settings(deadline=2000) def test_string_conversion(selenium_module_scope, s): with selenium_context_manager(selenium_module_scope) as selenium: # careful string escaping here -- hypothesis will fuzz it. sbytes = list(s.encode()) selenium.run_js( f""" - window.sjs = (new TextDecoder("utf8")).decode(new Uint8Array({sbytes})); + self.sjs = (new TextDecoder("utf8")).decode(new Uint8Array({sbytes})); pyodide.runPython('spy = bytes({sbytes}).decode()'); """ ) @@ -33,7 +33,7 @@ def test_string_conversion(selenium_module_scope, s): strategies.floats(allow_nan=False), ) ) -@settings(deadline=600) +@settings(deadline=2000) def test_number_conversions(selenium_module_scope, n): with selenium_context_manager(selenium_module_scope) as selenium: import json @@ -41,7 +41,7 @@ def test_number_conversions(selenium_module_scope, n): s = json.dumps(n) selenium.run_js( f""" - window.x_js = eval({s!r}); // JSON.parse apparently doesn't work + self.x_js = eval({s!r}); // JSON.parse apparently doesn't work pyodide.runPython(` import json x_py = json.loads({s!r}) @@ -60,7 +60,7 @@ def test_number_conversions(selenium_module_scope, n): def test_nan_conversions(selenium): selenium.run_js( """ - window.a = NaN; + self.a = NaN; pyodide.runPython(` from js import a from math import isnan, nan @@ -75,11 +75,11 @@ def test_nan_conversions(selenium): @given(n=strategies.integers()) -@settings(deadline=600) +@settings(deadline=2000) def test_bigint_conversions(selenium_module_scope, n): with selenium_context_manager(selenium_module_scope) as selenium: h = hex(n) - selenium.run_js(f"window.h = {h!r};") + selenium.run_js(f"self.h = {h!r};") selenium.run_js( """ let negative = false; @@ -88,9 +88,9 @@ def test_bigint_conversions(selenium_module_scope, n): h2 = h2.slice(1); negative = true; } - window.n = BigInt(h2); + self.n = BigInt(h2); if(negative){ - window.n = -n; + self.n = -n; } pyodide.runPython(` from js import n, h @@ -112,7 +112,7 @@ def test_bigint_conversions(selenium_module_scope, n): # Generate an object of any type @given(obj=from_type(type).flatmap(from_type)) -@settings(deadline=600) +@settings(deadline=2000) def test_hyp_py2js2py(selenium_module_scope, obj): with selenium_context_manager(selenium_module_scope) as selenium: import pickle @@ -143,7 +143,7 @@ def test_hyp_py2js2py(selenium_module_scope, obj): ) selenium.run_js( """ - window.x2 = pyodide.globals.get("x1"); + self.x2 = pyodide.globals.get("x1"); pyodide.runPython(` from js import x2 if x1 != x2: @@ -158,7 +158,7 @@ def test_big_integer_py2js2py(selenium): a = 9992361673228537 selenium.run_js( f""" - window.a = pyodide.runPython("{a}") + self.a = pyodide.runPython("{a}") pyodide.runPython(` from js import a assert a == {a} @@ -168,7 +168,7 @@ def test_big_integer_py2js2py(selenium): a = -a selenium.run_js( f""" - window.a = pyodide.runPython("{a}") + self.a = pyodide.runPython("{a}") pyodide.runPython(` from js import a assert a == {a} @@ -179,7 +179,7 @@ def test_big_integer_py2js2py(selenium): # Generate an object of any type @given(obj=from_type(type).flatmap(from_type)) -@settings(deadline=600) +@settings(deadline=2000) def test_hyp_tojs_no_crash(selenium_module_scope, obj): with selenium_context_manager(selenium_module_scope) as selenium: import pickle @@ -227,7 +227,7 @@ def test_python2js2(selenium): let xpy = pyodide.runPython("b'bytes'"); let x = xpy.toJs(); xpy.destroy(); - return (x instanceof window.Uint8Array) && + return (x.constructor.name === "Uint8Array") && (x.length === 5) && (x[0] === 98) """ @@ -241,7 +241,7 @@ def test_python2js3(selenium): let typename = proxy.type; let x = proxy.toJs(); proxy.destroy(); - return ((typename === "list") && (x instanceof window.Array) && + return ((typename === "list") && (x.constructor.name === "Array") && (x.length === 3) && (x[0] == 1) && (x[1] == 2) && (x[2] == 3)); """ ) @@ -280,24 +280,24 @@ def test_wrong_way_conversions(selenium): let t = new Test(); assert(() => pyodide.toPy(t) === t); - window.a1 = [1,2,3]; - window.b1 = pyodide.toPy(a1); - window.a2 = { a : 1, b : 2, c : 3}; - window.b2 = pyodide.toPy(a2); + self.a1 = [1,2,3]; + self.b1 = pyodide.toPy(a1); + self.a2 = { a : 1, b : 2, c : 3}; + self.b2 = pyodide.toPy(a2); pyodide.runPython(` from js import a1, b1, a2, b2 assert a1.to_py() == b1 assert a2.to_py() == b2 `); - window.b1.destroy(); - window.b2.destroy(); + self.b1.destroy(); + self.b2.destroy(); """ ) selenium.run_js( """ - window.a = [1,2,3]; - window.b = pyodide.runPython(` + self.a = [1,2,3]; + self.b = pyodide.runPython(` import pyodide pyodide.to_js([1, 2, 3]) `); @@ -307,7 +307,7 @@ def test_wrong_way_conversions(selenium): selenium.run_js( """ - window.t3 = pyodide.runPython(` + self.t3 = pyodide.runPython(` class Test: pass t1 = Test() t2 = pyodide.to_js(t1) @@ -367,7 +367,7 @@ def test_run_python_simple_error(selenium): def test_js2python(selenium): selenium.run_js( """ - window.test_objects = { + self.test_objects = { jsstring_ucs1 : "pyodidé", jsstring_ucs2 : "碘化物", jsstring_ucs4 : "🐍", @@ -382,7 +382,7 @@ def test_js2python(selenium): jspython : pyodide.globals.get("open"), jsbytes : new Uint8Array([1, 2, 3]), jsfloats : new Float32Array([1, 2, 3]), - jsobject : new XMLHttpRequest(), + jsobject : new TextDecoder(), }; """ ) @@ -412,7 +412,7 @@ def test_js2python(selenium): (jsfloats.tolist() == [1, 2, 3]) and (jsfloats.tobytes() == expected) """ ) - assert selenium.run('str(t.jsobject) == "[object XMLHttpRequest]"') + assert selenium.run('str(t.jsobject) == "[object TextDecoder]"') assert selenium.run("bool(t.jsobject) == True") assert selenium.run("bool(t.jsarray0) == False") assert selenium.run("bool(t.jsarray1) == True") @@ -422,17 +422,17 @@ def test_js2python(selenium): def test_js2python_bool(selenium): selenium.run_js( """ - window.f = ()=>{} - window.m0 = new Map(); - window.m1 = new Map([[0, 1]]); - window.s0 = new Set(); - window.s1 = new Set([0]); + self.f = ()=>{} + self.m0 = new Map(); + self.m1 = new Map([[0, 1]]); + self.s0 = new Set(); + self.s1 = new Set([0]); """ ) assert ( selenium.run( """ - from js import window, f, m0, m1, s0, s1 + from js import self, f, m0, m1, s0, s1 [bool(x) for x in [f, m0, m1, s0, s1]] """ ) @@ -457,7 +457,7 @@ def test_js2python_bool(selenium): def test_typed_arrays(selenium, jstype, pytype): assert selenium.run_js( f""" - window.array = new {jstype}([1, 2, 3, 4]); + self.array = new {jstype}([1, 2, 3, 4]); return pyodide.runPython(` from js import array array = array.to_py() @@ -477,7 +477,7 @@ def test_array_buffer(selenium): assert ( selenium.run_js( """ - window.array = new ArrayBuffer(100); + self.array = new ArrayBuffer(100); return pyodide.runPython(` from js import array array = array.to_py() @@ -490,7 +490,7 @@ def test_array_buffer(selenium): def assert_js_to_py_to_js(selenium, name): - selenium.run_js(f"window.obj = {name};") + selenium.run_js(f"self.obj = {name};") selenium.run("from js import obj") assert selenium.run_js( """ @@ -503,7 +503,7 @@ def assert_js_to_py_to_js(selenium, name): def assert_py_to_js_to_py(selenium, name): selenium.run_js( f""" - window.obj = pyodide.runPython('{name}'); + self.obj = pyodide.runPython('{name}'); pyodide.runPython(` from js import obj assert obj is {name} @@ -532,17 +532,17 @@ def test_recursive_dict_to_js(): def test_list_js2py2js(selenium): - selenium.run_js("window.x = [1,2,3];") + selenium.run_js("self.x = [1,2,3];") assert_js_to_py_to_js(selenium, "x") def test_dict_js2py2js(selenium): - selenium.run_js("window.x = { a : 1, b : 2, 0 : 3 };") + selenium.run_js("self.x = { a : 1, b : 2, 0 : 3 };") assert_js_to_py_to_js(selenium, "x") def test_error_js2py2js(selenium): - selenium.run_js("window.err = new Error('hello there?');") + selenium.run_js("self.err = new Error('hello there?');") assert_js_to_py_to_js(selenium, "err") @@ -570,7 +570,7 @@ def test_jsproxy_attribute_error(selenium): this.y = y; } } - window.point = new Point(42, 43); + self.point = new Point(42, 43); """ ) selenium.run( @@ -607,7 +607,7 @@ def test_javascript_error(selenium): def test_javascript_error_back_to_js(selenium): selenium.run_js( """ - window.err = new Error("This is a js error"); + self.err = new Error("This is a js error"); """ ) assert ( @@ -853,7 +853,7 @@ def test_tojs9(selenium): def test_to_py(selenium): result = selenium.run_js( """ - window.a = new Map([[1, [1,2,new Set([1,2,3])]], [2, new Map([[1,2],[2,7]])]]); + self.a = new Map([[1, [1,2,new Set([1,2,3])]], [2, new Map([[1,2],[2,7]])]]); a.get(2).set("a", a); let result = []; for(let i = 0; i < 4; i++){ @@ -874,7 +874,7 @@ def test_to_py(selenium): result = selenium.run_js( """ - window.a = { "x" : 2, "y" : 7, "z" : [1,2] }; + self.a = { "x" : 2, "y" : 7, "z" : [1,2] }; a.z.push(a); let result = []; for(let i = 0; i < 4; i++){ @@ -901,7 +901,7 @@ def test_to_py(selenium): this.y = 7; } } - window.a = new Temp(); + self.a = new Temp(); let result = pyodide.runPython(` from js import a b = a.to_py() @@ -916,7 +916,7 @@ def test_to_py(selenium): with pytest.raises(selenium.JavascriptException, match=msg): selenium.run_js( """ - window.a = new Map([[[1,1], 2]]); + self.a = new Map([[[1,1], 2]]); pyodide.runPython(` from js import a a.to_py() @@ -928,7 +928,7 @@ def test_to_py(selenium): with pytest.raises(selenium.JavascriptException, match=msg): selenium.run_js( """ - window.a = new Set([[1,1]]); + self.a = new Set([[1,1]]); pyodide.runPython(` from js import a a.to_py() @@ -940,7 +940,7 @@ def test_to_py(selenium): with pytest.raises(selenium.JavascriptException, match=msg): selenium.run_js( """ - window.a = new Map([[0, 2], [false, 3]]); + self.a = new Map([[0, 2], [false, 3]]); pyodide.runPython(` from js import a a.to_py() @@ -952,7 +952,7 @@ def test_to_py(selenium): with pytest.raises(selenium.JavascriptException, match=msg): selenium.run_js( """ - window.a = new Map([[1, 2], [true, 3]]); + self.a = new Map([[1, 2], [true, 3]]); pyodide.runPython(` from js import a a.to_py() @@ -964,7 +964,7 @@ def test_to_py(selenium): with pytest.raises(selenium.JavascriptException, match=msg): selenium.run_js( """ - window.a = new Set([0, false]); + self.a = new Set([0, false]); pyodide.runPython(` from js import a a.to_py() @@ -976,7 +976,7 @@ def test_to_py(selenium): with pytest.raises(selenium.JavascriptException, match=msg): selenium.run_js( """ - window.a = new Set([1, true]); + self.a = new Set([1, true]); pyodide.runPython(` from js import a a.to_py() diff --git a/src/tests/test_webloop.py b/src/tests/test_webloop.py index 3537c9114..61840855f 100644 --- a/src/tests/test_webloop.py +++ b/src/tests/test_webloop.py @@ -5,11 +5,11 @@ def run_with_resolve(selenium, code): selenium.run_js( f""" try {{ - let promise = new Promise((resolve) => window.resolve = resolve); + let promise = new Promise((resolve) => self.resolve = resolve); pyodide.runPython({code!r}); await promise; }} finally {{ - delete window.resolve; + delete self.resolve; }} """ ) @@ -173,13 +173,13 @@ def test_run_in_executor(selenium): def test_webloop_exception_handler(selenium): - selenium.run( + selenium.run_async( """ import asyncio async def test(): raise Exception("test") asyncio.ensure_future(test()) - pass + await asyncio.sleep(0.2) """ ) assert "Task exception was never retrieved" in selenium.logs diff --git a/tools/node_test_driver.js b/tools/node_test_driver.js new file mode 100644 index 000000000..205932da9 --- /dev/null +++ b/tools/node_test_driver.js @@ -0,0 +1,75 @@ +const vm = require("vm"); +const readline = require("readline"); +const path = require("path"); +const util = require("util"); +const node_fetch = require("node-fetch"); +const base64 = require("base-64"); + +require(path.resolve("./pyodide.js")); +let base_url = process.argv[2]; +// node requires full paths. +function fetch(path) { + return node_fetch(new URL(path, base_url).toString()); +} + +const context = Object.assign({}, globalThis, { + path, + process, + require, + fetch, + TextDecoder: util.TextDecoder, + TextEncoder: util.TextEncoder, + URL, + atob: base64.decode, + btoa: base64.encode, +}); +vm.createContext(context); +vm.runInContext("globalThis.self = globalThis;", context); + +// Get rid of all colors in output of console.log, they mess us up. +for (let key of Object.keys(util.inspect.styles)) { + util.inspect.styles[key] = undefined; +} + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, +}); + +let cur_code = ""; +let cur_uuid; +rl.on("line", async function (line) { + if (!cur_uuid) { + cur_uuid = line; + return; + } + if (line !== cur_uuid) { + cur_code += line + "\n"; + } else { + evalCode(cur_uuid, cur_code, context); + cur_code = ""; + cur_uuid = undefined; + } +}); + +async function evalCode(uuid, code, eval_context) { + let p = new Promise((resolve, reject) => { + eval_context.___outer_resolve = resolve; + eval_context.___outer_reject = reject; + }); + let wrapped_code = ` + (async function(){ + ${code} + })().then(___outer_resolve).catch(___outer_reject); + `; + let delim = uuid + ":UUID"; + console.log(delim); + try { + vm.runInContext(wrapped_code, eval_context); + let result = JSON.stringify(await p); + console.log(`${delim}\n0\n${result}\n${delim}`); + } catch (e) { + console.log(`${delim}\n1\n${e.stack}\n${delim}`); + } +} diff --git a/tools/testsetup.js b/tools/testsetup.js new file mode 100644 index 000000000..eca7b4c6a --- /dev/null +++ b/tools/testsetup.js @@ -0,0 +1,76 @@ +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); +};