MAINT Switch to pytest-pyodide (#2893)

Co-authored-by: ryanking13 <def6488@gmail.com>
This commit is contained in:
Roman Yurchak 2022-07-31 11:00:45 +01:00 committed by GitHub
parent 5d72fd427e
commit c85e00806b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 99 additions and 1982 deletions

View File

@ -25,7 +25,7 @@ jobs:
name: Test docs
command: |
mkdir test-results
pip install ./pyodide-test-runner
pip install pytest-pyodide
npm install -g node-fetch@2
pytest docs/sphinx_pyodide/tests --junitxml=test-results/junit.xml
- store_test_results:
@ -243,7 +243,7 @@ jobs:
command: |
make npm-link
mkdir test-results
pip install ./pyodide-test-runner
pip install pytest-pyodide
npm install -g node-fetch@2
if [ -z "<< parameters.cache-dir >>" ]; then
export CACHE_DIR=".test_cache/.pytest_cache_$(echo $RANDOM | md5sum | head -c 10)"
@ -269,22 +269,10 @@ jobs:
name: stack-size
command: |
make npm-link
pip install ./pyodide-test-runner
pip install pytest-pyodide
npm install -g node-fetch@2
pytest -s benchmark/stack_usage.py | sed -n 's/## //pg'
test-test-runner:
<<: *defaults
resource_class: medium+
steps:
- attach_workspace:
at: .
- run:
name: test
command: |
npm install -g node-fetch@2
cd pyodide-test-runner && pytest -v .
test-js:
<<: *defaults
resource_class: small
@ -322,7 +310,7 @@ jobs:
- run:
name: benchmark
command: |
pip install ./pyodide-test-runner
pip install pytest-pyodide
npm install -g node-fetch@2
python benchmark/benchmark.py all --output dist/benchmarks.json
@ -516,7 +504,7 @@ workflows:
- test-main:
name: test-core-chrome
test-params: -k "chrome and not webworker" src packages/micropip packages/fpcast-test packages/sharedlib-test-py/ packages/cpp-exceptions-test/
test-params: --runtime chrome -k "not webworker" src packages/micropip packages/fpcast-test packages/sharedlib-test-py/ packages/cpp-exceptions-test/
requires:
- build-core
filters:
@ -525,7 +513,7 @@ workflows:
- test-main:
name: test-core-firefox
test-params: -k "firefox and not webworker" src packages/micropip packages/fpcast-test packages/sharedlib-test-py/ packages/cpp-exceptions-test/
test-params: --runtime firefox -k "not webworker" src packages/micropip packages/fpcast-test packages/sharedlib-test-py/ packages/cpp-exceptions-test/
requires:
- build-core
filters:
@ -534,7 +522,7 @@ workflows:
- test-main:
name: test-core-node
test-params: -k node src packages/micropip packages/fpcast-test packages/sharedlib-test-py/ packages/cpp-exceptions-test/
test-params: --runtime node src packages/micropip packages/fpcast-test packages/sharedlib-test-py/ packages/cpp-exceptions-test/
requires:
- build-core
filters:
@ -543,7 +531,7 @@ workflows:
- test-main:
name: test-core-chrome-webworker
test-params: -k chrome src/tests/test_webworker.py
test-params: --runtime chrome src/tests/test_webworker.py
requires:
- test-core-chrome
filters:
@ -552,7 +540,7 @@ workflows:
- test-main:
name: test-core-firefox-webworker
test-params: -k firefox src/tests/test_webworker.py
test-params: --runtime firefox src/tests/test_webworker.py
requires:
- test-core-firefox
filters:
@ -561,7 +549,7 @@ workflows:
- test-main:
name: test-packages-chrome-no-numpy-dependents
test-params: -k chrome packages/test* packages/*/test*
test-params: --runtime chrome packages/test* packages/*/test*
cache-dir: .pytest_cache_chrome
requires:
- build-packages-no-numpy-dependents
@ -576,7 +564,7 @@ workflows:
- test-main:
name: test-packages-chrome
test-params: -k chrome packages/test* packages/*/test* --skip-passed
test-params: --runtime chrome packages/test* packages/*/test* --skip-passed
cache-dir: .pytest_cache_chrome
requires:
- test-packages-chrome-no-numpy-dependents
@ -587,7 +575,7 @@ workflows:
- test-main:
name: test-packages-firefox-no-numpy-dependents
test-params: -k firefox packages/test* packages/*/test*
test-params: --runtime firefox packages/test* packages/*/test*
cache-dir: .pytest_cache_firefox
requires:
- build-packages-no-numpy-dependents
@ -602,7 +590,7 @@ workflows:
- test-main:
name: test-packages-firefox
test-params: -k firefox packages/test* packages/*/test* --skip-passed
test-params: --runtime firefox packages/test* packages/*/test* --skip-passed
cache-dir: .pytest_cache_firefox
requires:
- test-packages-firefox-no-numpy-dependents
@ -613,7 +601,7 @@ workflows:
- test-main:
name: test-packages-node-no-numpy-dependents
test-params: -k node packages/test* packages/*/test*
test-params: --runtime node packages/test* packages/*/test*
cache-dir: .pytest_cache_node
requires:
- build-packages-no-numpy-dependents
@ -628,7 +616,7 @@ workflows:
- test-main:
name: test-packages-node
test-params: -k node packages/test* packages/*/test* --skip-passed
test-params: --runtime node packages/test* packages/*/test* --skip-passed
cache-dir: .pytest_cache_node
requires:
- test-packages-node-no-numpy-dependents
@ -637,13 +625,6 @@ workflows:
tags:
only: /.*/
- test-test-runner:
requires:
- build-core
filters:
tags:
only: /.*/
- test-js:
requires:
- build-core

View File

@ -33,8 +33,7 @@ jobs:
shell: bash -l {0}
run: |
mkdir test-results
python3 -m pip install ./pyodide-test-runner
python3 -m pip install -e ./pyodide-build
cd pyodide-build && python3 -m pip install -e ".[test]" && cd ..
python3 -m pip install pytest-cov hypothesis pytz
- name: Run tests
shell: bash -l {0}
@ -42,7 +41,7 @@ jobs:
PYODIDE_ROOT=. pytest \
--junitxml=test-results/junit.xml \
--verbose \
-k 'not (chrome or firefox or node)' \
--runtime host \
--cov=pyodide_build --cov=pyodide \
src pyodide-build packages/micropip/
- uses: codecov/codecov-action@v3
@ -157,7 +156,7 @@ jobs:
shell: bash -l {0}
run: |
pip install -r requirements.txt
pip install ./pyodide-test-runner
cd pyodide-build && pip install -e ".[test]" && cd ..
# FIXME: playwright 1.23.0 has unknown performance issue on firefox
pip install "playwright<1.23.0" && python -m playwright install
@ -170,7 +169,8 @@ jobs:
ls -lh
ls -lh dist/
tools/pytest_wrapper.py src packages/micropip/ \
-v -k "${BROWSER}" \
-v \
--runtime "${BROWSER}" \
--runner "${RUNNER}" \
--durations 50 \
--junitxml=test-results/core_test.xml
@ -184,7 +184,9 @@ jobs:
ls -lh
ls -lh dist/
tools/pytest_wrapper.py packages/test* packages/*/test* \
-v -k "numpy and not joblib and ${BROWSER}" \
-v \
-k "numpy and not joblib" \
--runtime "${BROWSER}" \
--runner "${RUNNER}" \
--durations 50 \
--junitxml=test-results/packages_test.xml

View File

@ -8,7 +8,7 @@ from time import time
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from pyodide_test_runner import ( # noqa: E402
from pytest_pyodide import ( # noqa: E402
SeleniumChromeWrapper,
SeleniumFirefoxWrapper,
spawn_web_server,

View File

@ -12,15 +12,15 @@ DIST_PATH = ROOT_PATH / "dist"
sys.path.append(str(ROOT_PATH / "pyodide-build"))
sys.path.append(str(ROOT_PATH / "src" / "py"))
import pyodide_test_runner.browser
from pyodide_test_runner.utils import maybe_skip_test
from pyodide_test_runner.utils import package_is_built as _package_is_built
from pyodide_test_runner.utils import parse_xfail_browsers
import pytest_pyodide.browser
from pytest_pyodide.utils import maybe_skip_test
from pytest_pyodide.utils import package_is_built as _package_is_built
from pytest_pyodide.utils import parse_xfail_browsers
# There are a bunch of global objects that occasionally enter the hiwire cache
# but never leave. The refcount checks get angry about them if they aren't preloaded.
# We need to go through and touch them all once to keep everything okay.
pyodide_test_runner.browser.INITIALIZE_SCRIPT = """
pytest_pyodide.browser.INITIALIZE_SCRIPT = """
pyodide.globals.get;
pyodide._api.pyodide_code.eval_code;
pyodide._api.pyodide_code.eval_code_async;

View File

@ -147,7 +147,7 @@ The tests should go in one or more files like
`test_<package-name>.py`. The tests should look like:
```py
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["<package-name>"])
def test_mytestname(selenium):
@ -160,7 +160,7 @@ If you want to run your package's full pytest test suite and your package
vendors tests you can do it like:
```py
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["<package-name>-tests", "pytest"])
def test_mytestname(selenium):

View File

@ -60,7 +60,6 @@ flexible.
There are 5 test locations that are collected by pytest:
- `src/tests/`: general Pyodide tests and tests running the CPython test suite
- `pyodide-test-runner/pyodide_test_runner/tests/`: Tests for the testing
system.
- `pyodide-build/pyodide_build/tests/`: tests related to Pyodide build system
(do not require selenium to run)
@ -163,10 +162,10 @@ commits, you will either have to pull in the remote changes or force push.
Many tests simply involve running a chunk of code in Pyodide and ensuring it
doesn't error. In this case, one can use the `run_in_pyodide` decorate from
`pyodide_test_runner.decorator`, e.g.
`pytest_pyodide.decorator`, e.g.
```python
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide
def test_add(selenium):
@ -178,7 +177,7 @@ decorator can also be called with a `packages` argument to load packages before
running the test. For example:
```python
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages = ["regex"])
def test_regex(selenium_standalone):
@ -201,7 +200,7 @@ innermost decorator. Any decorators inside of `@run_in_pyodide` will be have no
effect on the behavior of the test.
```python
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@pytest.mark.parametrize("x", [1, 2, 3])
@run_in_pyodide(packages = ["regex"])

View File

@ -225,7 +225,8 @@ substitutions:
{pr}`2510`, {pr}`2541`
- {{ Breaking }} `pyodide_build.testing` is removed. `run_in_pyodide`
decorator can now be accessed through `pyodide_test_runner`.
decorator can now be accessed through
[`pytest-runner`](https://github.com/pyodide/pytest-pyodide) package.
{pr}`2418`
## Version 0.20.0

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["Jinja2"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["RobotRaconteur", "numpy"])

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@pytest.mark.parametrize(

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["bitarray-tests"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["boost-histogram"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["cffi"])

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@pytest.mark.parametrize(

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["cloudpickle"])

View File

@ -1,7 +1,7 @@
from hypothesis import HealthCheck, given, settings
from hypothesis.strategies import binary, integers
from pyodide_test_runner import run_in_pyodide
from pyodide_test_runner.fixture import selenium_context_manager
from pytest_pyodide import run_in_pyodide
from pytest_pyodide.fixture import selenium_context_manager
@run_in_pyodide(packages=["cryptography"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["fpcast-test"])

View File

@ -1,7 +1,7 @@
from functools import reduce
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
# Need to skip_refcount_check because we use matplotlib
DECORATORS = [

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["gmpy2"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["numpy", "imageio"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["jedi"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["lazy-object-proxy"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["lxml"])

View File

@ -6,7 +6,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from pyodide_test_runner import run_in_pyodide, spawn_web_server
from pytest_pyodide import run_in_pyodide, spawn_web_server
sys.path.append(str(Path(__file__).resolve().parent / "src"))

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["msgpack"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["networkx"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@pytest.mark.xfail_browsers(

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
def test_numpy(selenium):

View File

@ -1,7 +1,7 @@
import base64
import pathlib
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
REFERENCE_IMAGES_PATH = pathlib.Path(__file__).parent / "reference-images"

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["pyclipper"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["pyproj"])

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
solvers = [
"cadical",

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["python_solvespace"])

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@pytest.mark.driver_timeout(30)

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["pyyaml"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["regex"])

View File

@ -3,7 +3,7 @@ from collections.abc import Callable
from typing import Any
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
if "CI" in os.environ:
xfail_browsers: Callable[[Any], Any] = pytest.mark.xfail_browsers(

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner.fixture import selenium_context_manager
from pytest_pyodide.fixture import selenium_context_manager
@pytest.mark.driver_timeout(40)

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@pytest.mark.driver_timeout(40)

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["shapely"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["sharedlib-test-py"])

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@pytest.mark.driver_timeout(40)

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["sqlalchemy"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["test", "ssl"], pytest_assert_rewrites=False)

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["sympy"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner.decorator import run_in_pyodide
from pytest_pyodide.decorator import run_in_pyodide
@run_in_pyodide(packages=["termcolor"])

View File

@ -2,7 +2,7 @@ import functools
import os
import pytest
from pyodide_test_runner import SeleniumWrapper
from pytest_pyodide import SeleniumWrapper
from conftest import ROOT_PATH, package_is_built
from pyodide_build.io import parse_package_config

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["wrapt"])

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["xarray"])

View File

@ -3,7 +3,7 @@ import base64
import pathlib
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
DEMO_PATH = pathlib.Path(__file__).parent / "test_data"
DATA_TRAIN = base64.b64encode((DEMO_PATH / "dermatology.data").read_bytes())

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide(packages=["numpy", "numcodecs", "zarr"])

View File

@ -180,6 +180,7 @@ def test_environment_var_substitution(monkeypatch):
)
@pytest.mark.xfail(reason="FIXME: emcc is not available during test")
def test_exports_node(tmp_path):
template = """
int l();

View File

@ -35,6 +35,7 @@ console_scripts =
[options.extras_require]
test =
pytest
pytest-pyodide
[options.packages.find]
where = .

View File

@ -1 +0,0 @@
# pyodide-test-runner

View File

@ -1,4 +0,0 @@
pytest_plugins = [
"pyodide_test_runner.hook",
"pyodide_test_runner.fixture",
]

View File

@ -1,29 +0,0 @@
from .browser import (
BrowserWrapper,
NodeWrapper,
PlaywrightChromeWrapper,
PlaywrightFirefoxWrapper,
PlaywrightWrapper,
SeleniumChromeWrapper,
SeleniumFirefoxWrapper,
SeleniumWrapper,
)
from .decorator import run_in_pyodide
from .fixture import * # noqa: F403, F401
from .server import spawn_web_server
from .utils import parse_driver_timeout, set_webdriver_script_timeout
__all__ = [
"BrowserWrapper",
"SeleniumWrapper",
"PlaywrightWrapper",
"SeleniumFirefoxWrapper",
"SeleniumChromeWrapper",
"PlaywrightChromeWrapper",
"PlaywrightFirefoxWrapper",
"NodeWrapper",
"set_webdriver_script_timeout",
"parse_driver_timeout",
"run_in_pyodide",
"spawn_web_server",
]

View File

@ -1,546 +0,0 @@
import json
import textwrap
from pathlib import Path
import pexpect
TEST_SETUP_CODE = """
Error.stackTraceLimit = Infinity;
// Fix globalThis is messed up in firefox see facebook/react#16606.
// Replace it with window.
globalThis.globalThis = globalThis.window || globalThis;
globalThis.sleep = function (s) {
return new Promise((resolve) => setTimeout(resolve, s));
};
globalThis.assert = function (cb, message = "") {
if (message !== "") {
message = "\\n" + message;
}
if (cb() !== true) {
throw new Error(
`Assertion failed: ${cb.toString().slice(6)}${message}`
);
}
};
globalThis.assertAsync = async function (cb, message = "") {
if (message !== "") {
message = "\\n" + message;
}
if ((await cb()) !== true) {
throw new Error(
`Assertion failed: ${cb.toString().slice(12)}${message}`
);
}
};
function checkError(err, errname, pattern, pat_str, thiscallstr) {
if (typeof pattern === "string") {
pattern = new RegExp(pattern);
}
if (!err) {
throw new Error(`${thiscallstr} failed, no error thrown`);
}
if (err.constructor.name !== errname) {
throw new Error(
`${thiscallstr} failed, expected error ` +
`of type '${errname}' got type '${err.constructor.name}'`
);
}
if (!pattern.test(err.message)) {
throw new Error(
`${thiscallstr} failed, expected error ` +
`message to match pattern ${pat_str} got:\n${err.message}`
);
}
}
globalThis.assertThrows = function (cb, errname, pattern) {
let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`;
let thiscallstr = `assertThrows(${cb.toString()}, "${errname}", ${pat_str})`;
let err = undefined;
try {
cb();
} catch (e) {
err = e;
}
checkError(err, errname, pattern, pat_str, thiscallstr);
};
globalThis.assertThrowsAsync = async function (cb, errname, pattern) {
let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`;
let thiscallstr = `assertThrowsAsync(${cb.toString()}, "${errname}", ${pat_str})`;
let err = undefined;
try {
await cb();
} catch (e) {
err = e;
}
checkError(err, errname, pattern, pat_str, thiscallstr);
};
""".strip()
class JavascriptException(Exception):
def __init__(self, msg, stack):
self.msg = msg
self.stack = stack
# In chrome the stack contains the message
if self.stack and self.stack.startswith(self.msg):
self.msg = ""
def __str__(self):
return "\n\n".join(x for x in [self.msg, self.stack] if x)
class BrowserWrapper:
browser = ""
JavascriptException = JavascriptException
def __init__(
self,
server_port,
server_hostname="127.0.0.1",
server_log=None,
load_pyodide=True,
script_timeout=20,
script_type="classic",
dist_dir=None,
*args,
**kwargs,
):
self.server_port = server_port
self.server_hostname = server_hostname
self.base_url = f"http://{self.server_hostname}:{self.server_port}"
self.server_log = server_log
self.script_type = script_type
self.dist_dir = dist_dir
self.driver = self.get_driver() # type: ignore[attr-defined]
self.set_script_timeout(script_timeout)
self.script_timeout = script_timeout
self.prepare_driver()
self.javascript_setup()
if load_pyodide:
self.load_pyodide()
self.initialize_global_hiwire_objects()
self.save_state()
self.restore_state()
def get_driver(self):
raise NotImplementedError()
def goto(self, page):
raise NotImplementedError()
def set_script_timeout(self, timeout):
raise NotImplementedError()
def quit(self):
raise NotImplementedError()
def refresh(self):
raise NotImplementedError()
def run_js_inner(self, code, check_code):
raise NotImplementedError()
def prepare_driver(self):
if self.script_type == "classic":
self.goto(f"{self.base_url}/test.html")
elif self.script_type == "module":
self.goto(f"{self.base_url}/module_test.html")
else:
raise Exception("Unknown script type to load!")
def javascript_setup(self):
self.run_js(
TEST_SETUP_CODE,
pyodide_checks=False,
)
def load_pyodide(self):
self.run_js(
"""
let pyodide = await loadPyodide({ fullStdLib: false, jsglobals : self });
self.pyodide = pyodide;
globalThis.pyodide = pyodide;
pyodide._api.inTestHoist = true; // improve some error messages for tests
"""
)
def initialize_global_hiwire_objects(self):
"""
There are a bunch of global objects that occasionally enter the hiwire cache
but never leave. The refcount checks get angry about them if they aren't preloaded.
We need to go through and touch them all once to keep everything okay.
"""
self.run_js(
"""
pyodide.globals.get;
pyodide._api.pyodide_code.eval_code;
pyodide._api.pyodide_code.eval_code_async;
pyodide._api.pyodide_code.find_imports;
pyodide._api.pyodide_ffi.register_js_module;
pyodide._api.pyodide_ffi.unregister_js_module;
pyodide._api.importlib.invalidate_caches;
pyodide._api.package_loader.unpack_buffer;
pyodide._api.package_loader.get_dynlibs;
pyodide._api.package_loader.sub_resource_hash;
pyodide.runPython("");
pyodide.pyimport("pyodide.ffi.wrappers").destroy();
"""
)
@property
def pyodide_loaded(self):
return self.run_js("return !!(self.pyodide && self.pyodide.runPython);")
@property
def logs(self):
logs = self.run_js("return self.logs;", pyodide_checks=False)
if logs is not None:
return "\n".join(str(x) for x in logs)
return ""
def clean_logs(self):
self.run_js("self.logs = []", pyodide_checks=False)
def run(self, code):
return self.run_js(
f"""
let result = pyodide.runPython({code!r});
if(result && result.toJs){{
let converted_result = result.toJs();
if(pyodide.isPyProxy(converted_result)){{
converted_result = undefined;
}}
result.destroy();
return converted_result;
}}
return result;
"""
)
def run_async(self, code):
return self.run_js(
f"""
await pyodide.loadPackagesFromImports({code!r})
let result = await pyodide.runPythonAsync({code!r});
if(result && result.toJs){{
let converted_result = result.toJs();
if(pyodide.isPyProxy(converted_result)){{
converted_result = undefined;
}}
result.destroy();
return converted_result;
}}
return result;
"""
)
def run_js(self, code, pyodide_checks=True):
"""Run JavaScript code and check for pyodide errors"""
if isinstance(code, str) and code.startswith("\n"):
# we have a multiline string, fix indentation
code = textwrap.dedent(code)
if pyodide_checks:
check_code = """
if(globalThis.pyodide && pyodide._module && pyodide._module._PyErr_Occurred()){
try {
pyodide._module._pythonexc2js();
} catch(e){
console.error(`Python exited with error flag set! Error was:\n${e.message}`);
// Don't put original error message in new one: we want
// "pytest.raises(xxx, match=msg)" to fail
throw new Error(`Python exited with error flag set!`);
}
}
"""
else:
check_code = ""
return self.run_js_inner(code, check_code)
def get_num_hiwire_keys(self):
return self.run_js("return pyodide._module.hiwire.num_keys();")
@property
def force_test_fail(self) -> bool:
return self.run_js("return !!pyodide._api.fail_test;")
def clear_force_test_fail(self):
self.run_js("pyodide._api.fail_test = false;")
def save_state(self):
self.run_js("self.__savedState = pyodide._api.saveState();")
def restore_state(self):
self.run_js(
"""
if(self.__savedState){
pyodide._api.restoreState(self.__savedState)
}
"""
)
def get_num_proxies(self):
return self.run_js("return pyodide._module.pyproxy_alloc_map.size")
def enable_pyproxy_tracing(self):
self.run_js("pyodide._module.enable_pyproxy_allocation_tracing()")
def disable_pyproxy_tracing(self):
self.run_js("pyodide._module.disable_pyproxy_allocation_tracing()")
def run_webworker(self, code):
if isinstance(code, str) and code.startswith("\n"):
# we have a multiline string, fix indentation
code = textwrap.dedent(code)
worker_file = (
"webworker_dev.js"
if self.script_type == "classic"
else "module_webworker_dev.js"
)
return self.run_js(
"""
let worker = new Worker('{}', {{ type: '{}' }});
let res = new Promise((res, rej) => {{
worker.onerror = e => rej(e);
worker.onmessage = e => {{
if (e.data.results) {{
res(e.data.results);
}} else {{
rej(e.data.error);
}}
}};
worker.postMessage({{ python: {!r} }});
}});
return await res
""".format(
f"http://{self.server_hostname}:{self.server_port}/{worker_file}",
self.script_type,
code,
),
pyodide_checks=False,
)
def load_package(self, packages):
self.run_js(f"await pyodide.loadPackage({packages!r})")
class SeleniumWrapper(BrowserWrapper):
def goto(self, page):
self.driver.get(page)
def set_script_timeout(self, timeout):
self.driver.set_script_timeout(timeout)
def quit(self):
self.driver.quit()
def refresh(self):
self.driver.refresh()
self.javascript_setup()
def run_js_inner(self, code, check_code):
wrapper = """
let cb = arguments[arguments.length - 1];
let run = async () => { %s }
(async () => {
try {
let result = await run();
%s
cb([0, result]);
} catch (e) {
cb([1, e.toString(), e.stack, e.message]);
}
})()
"""
retval = self.driver.execute_async_script(wrapper % (code, check_code))
if retval[0] == 0:
return retval[1]
else:
print("JavascriptException message: ", retval[3])
raise JavascriptException(retval[1], retval[2])
@property
def urls(self):
for handle in self.driver.window_handles:
self.driver.switch_to.window(handle)
yield self.driver.current_url
class PlaywrightWrapper(BrowserWrapper):
def __init__(self, browsers, *args, **kwargs):
self.browsers = browsers
super().__init__(*args, **kwargs)
def goto(self, page):
self.driver.goto(page)
def get_driver(self):
return self.browsers[self.browser].new_page()
def set_script_timeout(self, timeout):
# playwright uses milliseconds for timeout
self.driver.set_default_timeout(timeout * 1000)
def quit(self):
self.driver.close()
def refresh(self):
self.driver.reload()
self.javascript_setup()
def run_js_inner(self, code, check_code):
# playwright `evaluate` waits until primise to resolve,
# so we don't need to use a callback like selenium.
wrapper = """
let run = async () => { %s }
(async () => {
try {
let result = await run();
%s
return [0, result];
} catch (e) {
return [1, e.toString(), e.stack];
}
})()
"""
retval = self.driver.evaluate(wrapper % (code, check_code))
if retval[0] == 0:
return retval[1]
else:
raise JavascriptException(retval[1], retval[2])
class SeleniumFirefoxWrapper(SeleniumWrapper):
browser = "firefox"
def get_driver(self):
from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options
options = Options()
options.add_argument("--headless")
return Firefox(executable_path="geckodriver", options=options)
class SeleniumChromeWrapper(SeleniumWrapper):
browser = "chrome"
def get_driver(self):
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--js-flags=--expose-gc")
return Chrome(options=options)
def collect_garbage(self):
self.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {})
class PlaywrightChromeWrapper(PlaywrightWrapper):
browser = "chrome"
def collect_garbage(self):
client = self.driver.context.new_cdp_session(self.driver)
client.send("HeapProfiler.collectGarbage")
class PlaywrightFirefoxWrapper(PlaywrightWrapper):
browser = "firefox"
class NodeWrapper(BrowserWrapper):
browser = "node"
def init_node(self):
curdir = Path(__file__).parent
self.p = pexpect.spawn("/bin/bash", timeout=60)
self.p.setecho(False)
self.p.delaybeforesend = None
# disable canonical input processing mode to allow sending longer lines
# See: https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn.send
self.p.sendline("stty -icanon")
self.p.sendline(
f"node --expose-gc --experimental-wasm-bigint {curdir}/node_test_driver.js {self.base_url} {self.dist_dir}",
)
try:
self.p.expect_exact("READY!!")
except pexpect.exceptions.EOF:
raise JavascriptException("", self.p.before.decode())
def get_driver(self):
self._logs = []
self.init_node()
class NodeDriver:
def __getattr__(self, x):
raise NotImplementedError()
return NodeDriver()
def prepare_driver(self):
pass
def set_script_timeout(self, timeout):
self._timeout = timeout
def quit(self):
self.p.sendeof()
def refresh(self):
self.quit()
self.init_node()
self.javascript_setup()
def collect_garbage(self):
self.run_js("gc()")
@property
def logs(self):
return "\n".join(self._logs)
def clean_logs(self):
self._logs = []
def run_js_inner(self, code, check_code):
check_code = ""
wrapped = """
let result = await (async () => {{ {} }})();
{}
return result;
""".format(
code,
check_code,
)
from uuid import uuid4
cmd_id = str(uuid4())
self.p.sendline(cmd_id)
self.p.sendline(wrapped)
self.p.sendline(cmd_id)
self.p.expect_exact(f"{cmd_id}:UUID\r\n", timeout=self._timeout)
self.p.expect_exact(f"{cmd_id}:UUID\r\n")
if self.p.before:
self._logs.append(self.p.before.decode()[:-2].replace("\r", ""))
self.p.expect("[01]\r\n")
success = int(self.p.match[0].decode()[0]) == 0
self.p.expect_exact(f"\r\n{cmd_id}:UUID\r\n")
if success:
return json.loads(self.p.before.decode().replace("undefined", "null"))
else:
raise JavascriptException("", self.p.before.decode())

View File

@ -1,300 +0,0 @@
import ast
import pickle
import sys
from base64 import b64decode, b64encode
from collections.abc import Callable, Collection
from copy import deepcopy
from typing import Any
import pytest
from .hook import ORIGINAL_MODULE_ASTS, REWRITTEN_MODULE_ASTS
from .utils import package_is_built as _package_is_built
def package_is_built(package_name):
return _package_is_built(package_name, pytest.pyodide_dist_dir)
class SeleniumType:
JavascriptException: type
browser: str
def load_package(self, *args, **kwargs):
...
def run_async(self, code: str):
...
def _encode(obj: Any) -> str:
"""
Pickle and base 64 encode obj so we can send it to Pyodide using string
templating.
"""
return b64encode(pickle.dumps(obj)).decode()
def _create_outer_test_function(
run_test: Callable,
node: ast.stmt,
) -> Callable:
"""
Create the top level item: it will be called by pytest and it calls
run_test.
If the original function looked like:
@outer_decorators
@run_in_pyodide
@inner_decorators
<async?> def func(<selenium_arg_name>, arg1, arg2, arg3):
# do stuff
This wrapper looks like:
def <func_name>(<selenium_arg_name>, arg1, arg2, arg3):
run_test(<selenium_arg_name>, (arg1, arg2, arg3))
Any inner_decorators get ignored. Any outer_decorators get applied by
the Python interpreter via the normal mechanism
"""
node_args = deepcopy(node.args)
if not node_args.args:
raise ValueError(
f"Function {node.name} should take at least one argument whose name should start with 'selenium'"
)
selenium_arg_name = node_args.args[0].arg
if not selenium_arg_name.startswith("selenium"):
raise ValueError(
f"Function {node.name}'s first argument name '{selenium_arg_name}' should start with 'selenium'"
)
new_node = ast.FunctionDef(
name=node.name, args=node_args, body=[], lineno=1, decorator_list=[]
)
run_test_id = "run-test-not-valid-identifier"
# Make onwards call with two args:
# 1. <selenium_arg_name>
# 2. all other arguments in a tuple
func_body = ast.parse("return run_test(selenium_arg_name, (arg1, arg2, ...))").body
onwards_call = func_body[0].value
onwards_call.func = ast.Name(id=run_test_id, ctx=ast.Load())
onwards_call.args[0].id = selenium_arg_name # Set variable name
onwards_call.args[1].elts = [ # Set tuple elements
ast.Name(id=arg.arg, ctx=ast.Load()) for arg in node_args.args[1:]
]
# Add extra <selenium_arg_name> argument
new_node.body = func_body
# Make a best effort to show something that isn't total nonsense in the
# traceback for the generated function when there is an error.
# This will show:
# > run_test(selenium_arg_name, (arg1, arg2, ...))
# in the traceback.
def fake_body_for_traceback(arg1, arg2, selenium_arg_name):
run_test(selenium_arg_name, (arg1, arg2, ...))
# Adjust line numbers to point into our fake function
lineno = fake_body_for_traceback.__code__.co_firstlineno
ast.increment_lineno(new_node, lineno)
mod = ast.Module([new_node], type_ignores=[])
ast.fix_missing_locations(mod)
co = compile(mod, __file__, "exec")
# Need to give our code access to the actual "run_test" object which it
# invokes.
globs = {run_test_id: run_test}
exec(co, globs)
return globs[node.name]
class run_in_pyodide:
def __new__(cls, function: Callable | None = None, /, **kwargs):
if function:
# Probably we were used like:
#
# @run_in_pyodide
# def f():
# pass
return run_in_pyodide(**kwargs)(function)
else:
# Just do normal __new__ behavior
return object.__new__(cls)
def __init__(
self,
packages: Collection[str] = (),
pytest_assert_rewrites: bool = True,
*,
_force_assert_rewrites: bool = False,
):
"""
This decorator can be called in two ways --- with arguments and without
arguments. If it is called without arguments, then the `function` argument
catches the function the decorator is applied to. Otherwise, standalone and
packages are the actual arguments to the decorator.
See docs/testing.md for details on how to use this.
Parameters
----------
packages : List[str]
List of packages to load before running the test
pytest_assert_rewrites : bool, default = True
If True, use pytest assertion rewrites. This gives better error messages
when an assertion fails, but requires us to load pytest.
"""
self._pkgs = list(packages)
self._pytest_not_built = False
if (
pytest_assert_rewrites
and not package_is_built("pytest")
and not _force_assert_rewrites
):
pytest_assert_rewrites = False
self._pytest_not_built = True
if pytest_assert_rewrites:
self._pkgs.append("pytest")
self._module_asts_dict = (
REWRITTEN_MODULE_ASTS if pytest_assert_rewrites else ORIGINAL_MODULE_ASTS
)
if package_is_built("tblib"):
self._pkgs.append("tblib")
self._pytest_assert_rewrites = pytest_assert_rewrites
def _code_template(self, args: tuple) -> str:
"""
Unpickle function ast and its arguments, compile and call function, and
if the function is async await the result. Last, if there was an
exception, pickle it and send it back.
"""
return f"""
async def __tmp():
from base64 import b64encode, b64decode
import pickle
mod = pickle.loads(b64decode({_encode(self._mod)!r}))
args = pickle.loads(b64decode({_encode(args)!r}))
co = compile(mod, {self._module_filename!r}, "exec")
d = {{}}
exec(co, d)
def encode(x):
return b64encode(pickle.dumps(x)).decode()
try:
result = d[{self._func_name!r}](None, *args)
if {self._async_func}:
result = await result
return [0, encode(result)]
except BaseException as e:
try:
from tblib import pickling_support
pickling_support.install()
except ImportError:
pass
return [1, encode(e)]
try:
result = await __tmp()
finally:
del __tmp
result
"""
def _run_test(self, selenium: SeleniumType, args: tuple):
"""The main test runner, called from the AST generated in
_create_outer_test_function."""
code = self._code_template(args)
if self._pkgs:
selenium.load_package(self._pkgs)
r = selenium.run_async(code)
[status, result] = r
result = pickle.loads(b64decode(result))
if status:
raise result
else:
return result
def _generate_pyodide_ast(
self, module_ast: ast.Module, funcname: str, func_line_no: int
) -> tuple[ast.Module, bool, ast.expr]:
"""Generates appropriate AST for the test to run in Pyodide.
The test ast includes mypy magic imports and the test function definition.
This will be pickled and sent to Pyodide.
"""
nodes: list[ast.stmt] = []
it = iter(module_ast.body)
while True:
try:
node = next(it)
except StopIteration:
raise Exception(
f"Didn't find function {funcname} (line {func_line_no}) in module."
) from None
# We need to include the magic imports that pytest inserts
if (
isinstance(node, ast.Import)
and node.names[0].asname
and node.names[0].asname.startswith("@")
):
nodes.append(node)
if (
node.end_lineno
and node.end_lineno > func_line_no
and node.lineno < func_line_no
):
it = iter(node.body)
continue
# We also want the function definition for the current test
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if node.lineno < func_line_no:
continue
if node.name != funcname:
raise RuntimeError(
f"Internal run_in_pyodide error: looking for function '{funcname}' but found '{node.name}'"
)
self._async_func = isinstance(node, ast.AsyncFunctionDef)
node.decorator_list = []
nodes.append(node)
break
self._mod = ast.Module(nodes, type_ignores=[])
ast.fix_missing_locations(self._mod)
self._node = node
def __call__(self, f: Callable) -> Callable:
func_name = f.__name__
module_filename = sys.modules[f.__module__].__file__ or ""
module_ast = self._module_asts_dict[module_filename]
func_line_no = f.__code__.co_firstlineno
# _code_template needs this info.
self._generate_pyodide_ast(module_ast, func_name, func_line_no)
self._func_name = func_name
self._module_filename = module_filename
wrapper = _create_outer_test_function(self._run_test, self._node)
return wrapper

View File

@ -1,239 +0,0 @@
import contextlib
import os
from pathlib import Path
import pytest
from .browser import (
BrowserWrapper,
NodeWrapper,
PlaywrightChromeWrapper,
PlaywrightFirefoxWrapper,
SeleniumChromeWrapper,
SeleniumFirefoxWrapper,
)
from .server import spawn_web_server
from .utils import parse_driver_timeout, set_webdriver_script_timeout
@pytest.fixture(scope="module")
def playwright_browsers(request):
if request.config.option.runner.lower() != "playwright":
yield {}
else:
# import playwright here to allow running tests without playwright installation
try:
from playwright.sync_api import sync_playwright
except ImportError:
pytest.exit(
"playwright not installed. try `pip install playwright && python -m playwright install`",
returncode=1,
)
with sync_playwright() as p:
try:
chromium = p.chromium.launch(
args=[
"--js-flags=--expose-gc",
],
)
firefox = p.firefox.launch()
# webkit = p.webkit.launch()
except Exception as e:
pytest.exit(f"playwright failed to launch\n{e}", returncode=1)
try:
yield {
"chrome": chromium,
"firefox": firefox,
# "webkit": webkit,
}
finally:
chromium.close()
firefox.close()
# webkit.close()
@contextlib.contextmanager
def selenium_common(
request, web_server_main, load_pyodide=True, script_type="classic", browsers=None
):
"""Returns an initialized selenium object.
If `_should_skip_test` indicate that the test will be skipped,
return None, as initializing Pyodide for selenium is expensive
"""
server_hostname, server_port, server_log = web_server_main
runner_type = request.config.option.runner.lower()
cls: type[BrowserWrapper]
browser_set = {
("selenium", "firefox"): SeleniumFirefoxWrapper,
("selenium", "chrome"): SeleniumChromeWrapper,
("selenium", "node"): NodeWrapper,
("playwright", "firefox"): PlaywrightFirefoxWrapper,
("playwright", "chrome"): PlaywrightChromeWrapper,
("playwright", "node"): NodeWrapper,
}
cls = browser_set.get((runner_type, request.param))
if cls is None:
raise AssertionError(
f"Unknown runner or browser: {runner_type} / {request.param}"
)
dist_dir = Path(os.getcwd(), request.config.getoption("--dist-dir"))
runner = cls(
server_port=server_port,
server_hostname=server_hostname,
server_log=server_log,
load_pyodide=load_pyodide,
browsers=browsers,
script_type=script_type,
dist_dir=dist_dir,
)
try:
yield runner
finally:
runner.quit()
@pytest.fixture(params=["firefox", "chrome", "node"], scope="function")
def selenium_standalone(request, web_server_main, playwright_browsers):
with selenium_common(
request, web_server_main, browsers=playwright_browsers
) as selenium:
with set_webdriver_script_timeout(
selenium, script_timeout=parse_driver_timeout(request.node)
):
try:
yield selenium
finally:
print(selenium.logs)
@pytest.fixture(params=["firefox", "chrome", "node"], scope="module")
def selenium_esm(request, web_server_main, playwright_browsers):
with selenium_common(
request,
web_server_main,
load_pyodide=True,
browsers=playwright_browsers,
script_type="module",
) as selenium:
with set_webdriver_script_timeout(
selenium, script_timeout=parse_driver_timeout(request.node)
):
try:
yield selenium
finally:
print(selenium.logs)
@contextlib.contextmanager
def selenium_standalone_noload_common(
request, web_server_main, playwright_browsers, script_type="classic"
):
with selenium_common(
request,
web_server_main,
load_pyodide=False,
browsers=playwright_browsers,
script_type=script_type,
) as selenium:
with set_webdriver_script_timeout(
selenium, script_timeout=parse_driver_timeout(request.node)
):
try:
yield selenium
finally:
print(selenium.logs)
@pytest.fixture(params=["firefox", "chrome"], scope="function")
def selenium_webworker_standalone(
request, web_server_main, playwright_browsers, script_type
):
# Avoid loading the fixture if the test is going to be skipped
if request.param == "firefox" and script_type == "module":
pytest.skip("firefox does not support module type web worker")
with selenium_standalone_noload_common(
request, web_server_main, playwright_browsers, script_type=script_type
) as selenium:
yield selenium
@pytest.fixture(params=["firefox", "chrome", "node"], scope="function")
def selenium_standalone_noload(request, web_server_main, playwright_browsers):
"""Only difference between this and selenium_webworker_standalone is that
this also tests on node."""
with selenium_standalone_noload_common(
request, web_server_main, playwright_browsers
) as selenium:
yield selenium
# selenium instance cached at the module level
@pytest.fixture(params=["firefox", "chrome", "node"], scope="module")
def selenium_module_scope(request, web_server_main, playwright_browsers):
with selenium_common(
request, web_server_main, browsers=playwright_browsers
) as selenium:
yield selenium
# Hypothesis is unhappy with function scope fixtures. Instead, use the
# module scope fixture `selenium_module_scope` and use:
# `with selenium_context_manager(selenium_module_scope) as selenium`
@contextlib.contextmanager
def selenium_context_manager(selenium_module_scope):
try:
selenium_module_scope.clean_logs()
yield selenium_module_scope
finally:
print(selenium_module_scope.logs)
@pytest.fixture
def selenium(request, selenium_module_scope):
with selenium_context_manager(selenium_module_scope) as selenium:
with set_webdriver_script_timeout(
selenium, script_timeout=parse_driver_timeout(request.node)
):
yield selenium
@pytest.fixture(params=["firefox", "chrome"], scope="function")
def console_html_fixture(request, web_server_main, playwright_browsers):
with selenium_common(
request, web_server_main, load_pyodide=False, browsers=playwright_browsers
) as selenium:
selenium.goto(
f"http://{selenium.server_hostname}:{selenium.server_port}/console.html"
)
selenium.javascript_setup()
try:
yield selenium
finally:
print(selenium.logs)
@pytest.fixture(scope="session")
def web_server_main(request):
"""Web server that serves files in the dist directory"""
with spawn_web_server(request.config.option.dist_dir) as output:
yield output
@pytest.fixture(scope="session")
def web_server_secondary(request):
"""Secondary web server that serves files dist directory"""
with spawn_web_server(request.config.option.dist_dir) as output:
yield output
@pytest.fixture(params=["classic", "module"], scope="module")
def script_type(request):
return request.param

View File

@ -1,91 +0,0 @@
import ast
import sys
from copy import deepcopy
from pathlib import Path
from typing import Any
import pytest
from _pytest.assertion.rewrite import AssertionRewritingHook, rewrite_asserts
from _pytest.python import (
pytest_pycollect_makemodule as orig_pytest_pycollect_makemodule,
)
def pytest_configure(config):
config.addinivalue_line(
"markers",
"skip_refcount_check: Don't run refcount checks",
)
config.addinivalue_line(
"markers",
"skip_pyproxy_check: Don't run pyproxy allocation checks",
)
config.addinivalue_line(
"markers",
"driver_timeout: Set script timeout in WebDriver",
)
config.addinivalue_line(
"markers",
"xfail_browsers: xfail a test in specific browsers",
)
pytest.pyodide_dist_dir = config.getoption("--dist-dir")
@pytest.hookimpl(tryfirst=True)
def pytest_addoption(parser):
group = parser.getgroup("general")
group.addoption(
"--dist-dir",
action="store",
default="pyodide",
help="Path to the pyodide dist directory",
type=Path,
)
group.addoption(
"--runner",
default="selenium",
choices=["selenium", "playwright"],
help="Select testing frameworks, selenium or playwright (default: %(default)s)",
)
# Handling for pytest assertion rewrites
# First we find the pytest rewrite config. It's an attribute of the pytest
# assertion rewriting meta_path_finder, so we locate that to get the config.
def _get_pytest_rewrite_config() -> Any:
for meta_path_finder in sys.meta_path:
if isinstance(meta_path_finder, AssertionRewritingHook):
break
else:
return None
return meta_path_finder.config
# Now we need to parse the ast of the files, rewrite the ast, and store the
# original and rewritten ast into dictionaries. `run_in_pyodide` will look the
# ast up in the appropriate dictionary depending on whether or not it is using
# pytest assert rewrites.
REWRITE_CONFIG = _get_pytest_rewrite_config()
del _get_pytest_rewrite_config
ORIGINAL_MODULE_ASTS: dict[str, ast.Module] = {}
REWRITTEN_MODULE_ASTS: dict[str, ast.Module] = {}
def pytest_pycollect_makemodule(module_path: Path, path: Any, parent: Any) -> None:
source = module_path.read_bytes()
strfn = str(module_path)
tree = ast.parse(source, filename=strfn)
ORIGINAL_MODULE_ASTS[strfn] = tree
tree2 = deepcopy(tree)
rewrite_asserts(tree2, source, strfn, REWRITE_CONFIG)
REWRITTEN_MODULE_ASTS[strfn] = tree2
orig_pytest_pycollect_makemodule(module_path, parent)

View File

@ -1,60 +0,0 @@
import pickle
from zoneinfo import ZoneInfo
from hypothesis import HealthCheck, settings, strategies
def is_picklable(x):
try:
pickle.dumps(x)
return True
except Exception:
return False
def is_equal_to_self(x):
try:
return x == x
except Exception:
return False
try:
from exceptiongroup import ExceptionGroup
except ImportError:
class ExceptionGroup:
pass
# Generate an object of any type
any_strategy = (
strategies.from_type(type)
.flatmap(strategies.from_type)
.filter(lambda x: not isinstance(x, ZoneInfo))
.filter(is_picklable)
.filter(lambda x: not isinstance(x, ExceptionGroup))
)
any_equal_to_self_strategy = any_strategy.filter(is_equal_to_self)
std_hypothesis_settings = settings(
deadline=2000,
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
def is_picklable(x):
try:
pickle.dumps(x)
return True
except Exception:
return False
strategy = (
strategies.from_type(type)
.flatmap(strategies.from_type)
.filter(lambda x: not isinstance(x, ZoneInfo))
.filter(is_picklable)
)

View File

@ -1,84 +0,0 @@
const vm = require("vm");
const readline = require("readline");
const path = require("path");
const util = require("util");
const node_fetch = require("node-fetch");
let baseUrl = process.argv[2];
let distDir = process.argv[3];
let { loadPyodide } = require(`${distDir}/pyodide`);
process.chdir(distDir);
// node requires full paths.
function fetch(path) {
return node_fetch(new URL(path, baseUrl).toString());
}
const context = {
loadPyodide,
path,
process,
require,
fetch,
setTimeout,
TextDecoder: util.TextDecoder,
TextEncoder: util.TextEncoder,
URL,
clearInterval,
clearTimeout,
setInterval,
setTimeout,
};
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}`);
}
}
console.log("READY!!");
// evalCode("xxx", "let pyodide = await loadPyodide(); pyodide.runPython(`print([x*x+1 for x in range(10)])`);", context);

View File

@ -1,82 +0,0 @@
import contextlib
import http.server
import multiprocessing
import os
import pathlib
import queue
import shutil
import socketserver
import sys
import tempfile
@contextlib.contextmanager
def spawn_web_server(dist_dir):
tmp_dir = tempfile.mkdtemp()
log_path = pathlib.Path(tmp_dir) / "http-server.log"
q: multiprocessing.Queue[str] = multiprocessing.Queue()
p = multiprocessing.Process(target=run_web_server, args=(q, log_path, dist_dir))
try:
p.start()
port = q.get()
hostname = "127.0.0.1"
print(
f"Spawning webserver at http://{hostname}:{port} "
f"(see logs in {log_path})"
)
yield hostname, port, log_path
finally:
q.put("TERMINATE")
p.join()
shutil.rmtree(tmp_dir)
def run_web_server(q, log_filepath, dist_dir):
"""Start the HTTP web server
Parameters
----------
q : Queue
communication queue
log_path : pathlib.Path
path to the file where to store the logs
"""
os.chdir(dist_dir)
log_fh = log_filepath.open("w", buffering=1)
sys.stdout = log_fh
sys.stderr = log_fh
class Handler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format_, *args):
print(
"[%s] source: %s:%s - %s"
% (self.log_date_time_string(), *self.client_address, format_ % args)
)
def end_headers(self):
# Enable Cross-Origin Resource Sharing (CORS)
self.send_header("Access-Control-Allow-Origin", "*")
super().end_headers()
with socketserver.TCPServer(("", 0), Handler) as httpd:
host, port = httpd.server_address
print(f"Starting webserver at http://{host}:{port}")
httpd.server_name = "test-server" # type: ignore[attr-defined]
httpd.server_port = port # type: ignore[attr-defined]
q.put(port)
def service_actions():
try:
if q.get(False) == "TERMINATE":
print("Stopping server...")
sys.exit(0)
except queue.Empty:
pass
httpd.service_actions = service_actions # type: ignore[assignment]
httpd.serve_forever()

View File

@ -1,198 +0,0 @@
import pytest
from hypothesis import given, settings
from pyodide_test_runner.decorator import run_in_pyodide
from pyodide_test_runner.hypothesis import any_strategy, std_hypothesis_settings
from pyodide_test_runner.utils import parse_driver_timeout
@run_in_pyodide
def example_func(selenium):
pass
@run_in_pyodide(_force_assert_rewrites=True)
def test_selenium1(selenium):
import pytest
with pytest.raises(AssertionError, match="assert 6 == 7"):
x = 6
y = 7
assert x == y
run_in_pyodide_alias = run_in_pyodide(_force_assert_rewrites=True)
@run_in_pyodide_alias
def test_selenium2(selenium):
import pytest
x = 6
y = 7
with pytest.raises(AssertionError, match="assert 6 == 7"):
assert x == y
@run_in_pyodide(_force_assert_rewrites=True)
async def test_selenium3(selenium):
from asyncio import sleep
import pytest
await sleep(0.01)
x = 6
await sleep(0.01)
y = 7
with pytest.raises(AssertionError, match="assert 6 == 7"):
assert x == y
def test_inner_function_closure_error(selenium):
x = 6
@run_in_pyodide
def inner_function(selenium):
assert x == 6
return 7
with pytest.raises(NameError, match="'x' is not defined"):
inner_function(selenium)
def test_inner_function(selenium):
@run_in_pyodide
def inner_function(selenium, x):
assert x == 6
return 7
assert inner_function(selenium, 6) == 7
def complicated_decorator(attr_name: str):
def inner_func(value):
def dec(func):
def wrapper(*args, **kwargs):
wrapper.dec_info.append((attr_name, value))
return func(*args, **kwargs)
wrapper.dec_info = getattr(func, "dec_info", [])
wrapper.__name__ = func.__name__
return wrapper
return dec
return inner_func
d1 = complicated_decorator("testdec1")
d2 = complicated_decorator("testdec2")
@d1("a")
@d2("b")
@d1("c")
@run_in_pyodide
def example_decorator_func(selenium):
pass
def test_selenium4(selenium_standalone):
example_decorator_func(selenium_standalone)
assert example_decorator_func.dec_info[-3:] == [
("testdec1", "a"),
("testdec2", "b"),
("testdec1", "c"),
]
def test_local_fail_load_package(selenium_standalone):
selenium = selenium_standalone
def _load_package_error(*args, **kwargs):
raise OSError("STOP!")
selenium.load_package = _load_package_error
exc = None
try:
example_func(selenium)
except OSError:
exc = pytest.ExceptionInfo.from_current()
assert exc
try:
exc.getrepr()
except IndexError as e:
import traceback
traceback.print_exception(e)
raise Exception(
"run_in_pyodide decorator badly messed up the line numbers."
" This could crash pytest. Printed the traceback to stdout."
)
@run_in_pyodide
def test_trivial1(selenium):
x = 7
assert x == 7
@run_in_pyodide()
def test_trivial2(selenium):
x = 7
assert x == 7
@run_in_pyodide(pytest_assert_rewrites=False)
def test_trivial3(selenium):
x = 7
assert x == 7
@pytest.mark.parametrize("jinja2", ["jINja2", "Jinja2"])
@run_in_pyodide
def test_parametrize(selenium, jinja2):
try:
assert jinja2.lower() == "jinja2"
except Exception as e:
print(e)
@pytest.mark.skip(reason="Nope!")
@run_in_pyodide(pytest_assert_rewrites=False)
def test_skip(selenium):
x = 6
assert x == 7
@run_in_pyodide
async def test_run_in_pyodide_async(selenium):
from asyncio import sleep
x = 6
await sleep(0.01)
assert x == 6
@pytest.mark.skip_refcount_check
@pytest.mark.skip_pyproxy_check
@given(obj=any_strategy)
@settings(
std_hypothesis_settings,
max_examples=25,
)
@run_in_pyodide
def test_hypothesis(selenium_standalone, obj):
from pyodide import to_js
to_js(obj)
run_in_pyodide_inner = run_in_pyodide()
run_in_pyodide_alias2 = pytest.mark.driver_timeout(40)(run_in_pyodide_inner)
@run_in_pyodide_alias2
def test_run_in_pyodide_alias(request):
assert parse_driver_timeout(request.node) == 40

View File

@ -1,66 +0,0 @@
def test_assert(selenium):
selenium.run_js(
r"""
let shouldPass;
shouldPass = true;
assert(() => shouldPass, "blah");
shouldPass = false;
let threw = false;
try {
assert(() => shouldPass, "blah");
} catch(e){
threw = true;
if(e.message !== `Assertion failed: shouldPass\nblah`){
throw new Error(`Unexpected message:\n${e.message}`);
}
}
if(!threw){
throw new Error("Didn't throw!");
}
"""
)
def test_assert_throws(selenium):
selenium.run_js(
r"""
let shouldPass;
let threw;
assertThrows(() => { throw new TypeError("aaabbbccc") }, "TypeError", "bbc");
assertThrows(() => { throw new TypeError("aaabbbccc") }, "TypeError", /.{3}.{3}.{3}/);
threw = false;
try {
assertThrows(() => 0, "TypeError", /.*/);
} catch(e) {
threw = true;
assert(() => e.message == `assertThrows(() => 0, "TypeError", /.*/) failed, no error thrown`, e.message);
}
assert(() => threw);
threw = false;
try {
assertThrows(() => { throw new ReferenceError("blah"); }, "TypeError", /.*/);
} catch(e) {
threw = true;
assert(() => e.message.endsWith("expected error of type 'TypeError' got type 'ReferenceError'"));
}
assert(() => threw);
threw = false;
try {
assertThrows(() => { throw new TypeError("blah"); }, "TypeError", "abcd");
} catch(e) {
threw = true;
console.log(`!!${e.message}!!`);
assert(() => e.message.endsWith(`expected error message to match pattern "abcd" got:\nblah`));
}
assert(() => threw);
threw = false;
try {
assertThrows(() => { throw new TypeError("blah"); }, "TypeError", /a..d/);
} catch(e) {
threw = true;
console.log(`!!${e.message}!!`);
assert(() => e.message.endsWith(`expected error message to match pattern /a..d/ got:\nblah`));
}
assert(() => threw);
"""
)

View File

@ -1,7 +0,0 @@
import pathlib
def test_web_server_secondary(selenium, web_server_secondary):
host, port, logs = web_server_secondary
assert pathlib.Path(logs).exists()
assert selenium.server_port != port

View File

@ -1,102 +0,0 @@
import contextlib
import functools
import json
import re
from pathlib import Path
import pytest
@contextlib.contextmanager
def set_webdriver_script_timeout(selenium, script_timeout: float | None):
"""Set selenium script timeout
Parameters
----------
selenum : SeleniumWrapper
a SeleniumWrapper wrapper instance
script_timeout : int | float
value of the timeout in seconds
"""
if script_timeout is not None:
selenium.set_script_timeout(script_timeout)
yield
# revert to the initial value
if script_timeout is not None:
selenium.set_script_timeout(selenium.script_timeout)
def parse_driver_timeout(node) -> float | None:
"""Parse driver timeout value from pytest request object"""
mark = node.get_closest_marker("driver_timeout")
if mark is None:
return None
else:
return mark.args[0]
def parse_xfail_browsers(node) -> dict[str, str]:
mark = node.get_closest_marker("xfail_browsers")
if mark is None:
return {}
return mark.kwargs
def maybe_skip_test(item, dist_dir, delayed=False):
"""If necessary skip test at the fixture level, to avoid
loading the selenium_standalone fixture which takes a long time.
"""
browsers = "|".join(["firefox", "chrome", "node"])
skip_msg = None
# Testing a package. Skip the test if the package is not built.
match = re.match(
r".*/packages/(?P<name>[\w\-]+)/test_[\w\-]+\.py", str(item.parent.fspath)
)
if match:
package_name = match.group("name")
if not package_is_built(package_name, dist_dir) and re.match(
rf"test_[\w\-]+\[({browsers})[^\]]*\]", item.name
):
skip_msg = f"package '{package_name}' is not built."
# Common package import test. Skip it if the package is not built.
if (
skip_msg is None
and str(item.fspath).endswith("test_packages_common.py")
and item.name.startswith("test_import")
):
match = re.match(rf"test_import\[({browsers})-(?P<name>[\w-]+)\]", item.name)
if match:
package_name = match.group("name")
if not package_is_built(package_name, dist_dir):
# If the test is going to be skipped remove the
# selenium_standalone as it takes a long time to initialize
skip_msg = f"package '{package_name}' is not built."
else:
raise AssertionError(
f"Couldn't parse package name from {item.name}. This should not happen!"
)
# TODO: also use this hook to skip doctests we cannot run (or run them
# inside the selenium wrapper)
if skip_msg is not None:
if delayed:
item.add_marker(pytest.mark.skip(reason=skip_msg))
else:
pytest.skip(skip_msg)
@functools.cache
def built_packages(dist_dir: Path) -> list[str]:
"""Returns the list of built package names from repodata.json"""
repodata_path = dist_dir / "repodata.json"
if not repodata_path.exists():
return []
return list(json.loads(repodata_path.read_text())["packages"].keys())
def package_is_built(package_name: str, dist_dir: Path) -> bool:
return package_name.lower() in built_packages(dist_dir)

View File

@ -1,4 +0,0 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

View File

@ -1,49 +0,0 @@
[metadata]
name = pyodide-test-runner
version = 0.21.0.dev0
author = Pyodide developers
description = "Pytest plugin for testing Pyodide and third-party applications that use Pyodide"
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/pyodide/pyodide
project_urls =
Bug Tracker = https://github.com/pyodide/pyodide/issues
Documentation = https://pyodide.org/en/stable/
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Operating System :: OS Independent
Framework :: Pytest
[options]
package_dir =
= .
packages = find:
python_requires = >=3.10
include_package_data = True
install_requires =
pexpect
pytest
pytest-asyncio
selenium
tblib
# This is required to add node driver code to the package.
[options.package_data]
pyodide_test_runner = *.js
# pytest will look up `pytest11` entrypoints to find plugins
# See: https://docs.pytest.org/en/7.1.x/how-to/writing_plugins.html#making-your-plugin-installable-by-others
[options.entry_points]
pytest11 =
pyodide_test_runner = pyodide_test_runner.fixture
pyodide_test_runner_hook = pyodide_test_runner.hook
[options.packages.find]
where = .
[tool:pytest]
asyncio_mode = strict
addopts =
--tb=short
--dist-dir=../dist

View File

@ -7,7 +7,6 @@ norecursedirs =
addopts =
--doctest-modules
--ignore="packages/matplotlib/src"
--ignore="pyodide-test-runner"
--tb=short
--dist-dir=dist

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide

View File

@ -3,7 +3,7 @@ import sys
import time
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
from pyodide import console
from pyodide.code import CodeRunner # noqa: E402

View File

@ -1,6 +1,6 @@
# See also test_typeconversions, and test_python.
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
def test_jsproxy_dir(selenium):

View File

@ -4,7 +4,7 @@ from textwrap import dedent
from typing import Any
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
from pyodide.code import CodeRunner, eval_code, find_imports, should_quiet # noqa: E402

View File

@ -1,5 +1,5 @@
import pytest
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@pytest.mark.xfail_browsers(node="XMLHttpRequest is not available in node")

View File

@ -1,4 +1,4 @@
from pyodide_test_runner import run_in_pyodide
from pytest_pyodide import run_in_pyodide
@run_in_pyodide

View File

@ -4,9 +4,9 @@ from typing import Any
import pytest
from hypothesis import example, given, settings, strategies
from hypothesis.strategies import text
from pyodide_test_runner import run_in_pyodide
from pyodide_test_runner.fixture import selenium_context_manager
from pyodide_test_runner.hypothesis import (
from pytest_pyodide import run_in_pyodide
from pytest_pyodide.fixture import selenium_context_manager
from pytest_pyodide.hypothesis import (
any_equal_to_self_strategy,
any_strategy,
std_hypothesis_settings,

View File

@ -64,11 +64,6 @@ PYTHON_TARGETS = [
build_version_pattern(r"version\s*=\s*{{{python_version}}}"),
prerelease=False,
),
Target(
ROOT / "pyodide-test-runner/setup.cfg",
build_version_pattern("version = {python_version}"),
prerelease=False,
),
]
JS_TARGETS = [