Set up pytest node tests (#1717)

This commit is contained in:
Hood Chatham 2021-07-20 08:48:27 +00:00 committed by GitHub
parent c8436c33a7
commit f0bd568a31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 695 additions and 399 deletions

View File

@ -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

View File

@ -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 \

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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()
`);

View File

@ -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")

View File

@ -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

View File

@ -29,6 +29,7 @@ def registered_packages_meta():
UNSUPPORTED_PACKAGES: dict = {
"chrome": ["scikit-image", "statsmodels"],
"firefox": [],
"node": ["scikit-image", "statsmodels"],
}

View File

@ -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]]:

View File

@ -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

20
requirements.txt Normal file
View File

@ -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

View File

@ -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"

View File

@ -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");
}

View File

@ -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",

View File

@ -28,6 +28,7 @@
}
},
"dependencies": {
"base-64": "^1.0.0",
"node-fetch": "^2.6.1"
}
}

View File

@ -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.

View File

@ -1,11 +1,8 @@
<!-- Bootstrap HTML for running the unit tests. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script type="text/javascript">
function sleep(s) {
return new Promise((resolve) => setTimeout(resolve, s));
}
window.logs = [];
console.log = function (message) {
window.logs.push(message);
@ -19,7 +16,6 @@
console.error = function (message) {
window.logs.push(message);
};
// set the pyodide files URL (packages.json, pyodide.asm.data etc)
</script>
<script src="./pyodide.js"></script>
</head>

View File

@ -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

View File

@ -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")

View File

@ -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:

View File

@ -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")"""
)

View File

@ -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)

View File

@ -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')"
)

View File

@ -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 "<exec>", line 8 in h
File "<exec>", line 6 in g
File "<exec>", line 4 in f
File "<exec>", line 9 in <module>
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 "<exec>", line 8 in h
File "<exec>", line 6 in g
File "<exec>", line 4 in f
File "<exec>", line 9 in <module>
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()
)

View File

@ -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("<Foo"));
@ -87,10 +88,10 @@ def test_pyproxy_refcount(selenium):
return pyodide.runPython("sys.getrefcount(pyfunc)");
}
let result = [];
window.jsfunc = function (f) { f(); };
self.jsfunc = function (f) { f(); };
pyodide.runPython(`
import sys
from js import window
from js import self
def pyfunc(*args, **kwargs):
print(*args, **kwargs)
@ -110,13 +111,13 @@ def test_pyproxy_refcount(selenium):
// 3. pyfunc is referenced from the sys.getrefcount()-test below
pyodide.runPython(`
window.jsfunc(pyfunc) # creates new PyProxy
self.jsfunc(pyfunc) # creates new PyProxy
`);
result.push([getRefCount(), 3])
pyodide.runPython(`
window.jsfunc(pyfunc) # create new PyProxy
window.jsfunc(pyfunc) # create new PyProxy
self.jsfunc(pyfunc) # create new PyProxy
self.jsfunc(pyfunc) # create new PyProxy
`)
// the refcount should be 3 because:
@ -294,7 +295,7 @@ def test_get_empty_buffer(selenium):
def test_pyproxy_get_buffer_type_argument(selenium, array_type):
selenium.run_js(
"""
window.a = pyodide.runPython("bytes(range(256))");
self.a = pyodide.runPython("bytes(range(256))");
"""
)
try:
@ -324,7 +325,7 @@ def test_pyproxy_get_buffer_type_argument(selenium, array_type):
else:
assert result == list(mv.cast(fmt))
finally:
selenium.run_js("a.destroy(); window.a = undefined;")
selenium.run_js("a.destroy(); self.a = undefined;")
def test_pyproxy_mixins(selenium):
@ -528,7 +529,7 @@ def test_pyproxy_mixins6(selenium):
@pytest.mark.skip_pyproxy_check
def test_pyproxy_gc(selenium):
if selenium.browser != "chrome":
if not hasattr(selenium, "collect_garbage"):
pytest.skip("No gc exposed")
# Two ways to trigger garbage collection in Chrome:
@ -542,16 +543,22 @@ def test_pyproxy_gc(selenium):
selenium.run_js(
"""
window.x = new FinalizationRegistry((val) => { 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");
"""
)

View File

@ -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": "*"}
)

View File

@ -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()

View File

@ -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

75
tools/node_test_driver.js Normal file
View File

@ -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}`);
}
}

76
tools/testsetup.js Normal file
View File

@ -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);
};