From c85e00806b38b23efc2b2c656457fcd8117cfee3 Mon Sep 17 00:00:00 2001 From: Roman Yurchak Date: Sun, 31 Jul 2022 11:00:45 +0100 Subject: [PATCH] MAINT Switch to pytest-pyodide (#2893) Co-authored-by: ryanking13 --- .circleci/config.yml | 49 +- .github/workflows/main.yml | 14 +- benchmark/benchmark.py | 2 +- conftest.py | 10 +- docs/development/new-packages.md | 4 +- docs/development/testing.md | 9 +- docs/project/changelog.md | 3 +- packages/Jinja2/test_jinja2.py | 2 +- packages/Pillow/test_pillow.py | 2 +- .../test_robotraconteur_pyodide.py | 2 +- packages/astropy/test_astropy.py | 2 +- packages/bitarray/test_bitarray.py | 2 +- .../boost-histogram/test_boost_histogram.py | 2 +- packages/cffi/test_cffi.py | 2 +- packages/cffi_example/test_cffi_example.py | 2 +- packages/cloudpickle/test_cloudpickle.py | 2 +- packages/cryptography/test_cryptography.py | 4 +- packages/fpcast-test/test_fpcast_test.py | 2 +- packages/galpy/test_galpy.py | 2 +- packages/gmpy2/test_gmpy2.py | 2 +- packages/imageio/test_imageio.py | 2 +- packages/jedi/test_jedi.py | 2 +- .../test_lazy_object_proxy.py | 2 +- packages/lxml/test_lxml.py | 2 +- packages/micropip/test_micropip.py | 2 +- packages/msgpack/test_pack.py | 2 +- packages/msprime/test_msprime.py | 2 +- packages/networkx/test_networkx.py | 2 +- packages/nlopt/test_nlopt.py | 2 +- packages/numcodecs/test_numcodecs.py | 2 +- packages/numpy/test_numpy.py | 2 +- packages/opencv-python/test_opencv_python.py | 2 +- packages/pyclipper/test_pyclipper.py | 2 +- packages/pyproj/test_pyproj.py | 2 +- packages/python-sat/test_python_sat.py | 2 +- .../test_python_solvespace.py | 2 +- packages/pywavelets/test_pywt.py | 2 +- packages/pyyaml/test_pyyaml.py | 2 +- packages/rebound/test_rebound.py | 2 +- packages/regex/test_regex.py | 2 +- packages/scikit-image/test_skimage.py | 2 +- packages/scikit-learn/test_scikit-learn.py | 2 +- packages/scipy/test_scipy.py | 2 +- packages/shapely/test_shapely.py | 2 +- packages/sharedlib-test-py/test_sharedlib.py | 2 +- packages/sparseqr/test_sparseqr.py | 2 +- packages/sqlalchemy/test_sqlalchemy.py | 2 +- packages/ssl/test_ssl.py | 2 +- packages/sympy/test_sympy.py | 2 +- packages/termcolor/test_termcolor.py | 2 +- packages/test_packages_common.py | 2 +- packages/tskit/test_tskit.py | 2 +- packages/wrapt/test_wrapt.py | 2 +- packages/xarray/test_xarray.py | 2 +- packages/xgboost/test_xgboost.py | 2 +- packages/zarr/test_zarr.py | 2 +- .../pyodide_build/tests/test_pywasmcross.py | 1 + pyodide-build/setup.cfg | 1 + pyodide-test-runner/README.md | 1 - pyodide-test-runner/conftest.py | 4 - .../pyodide_test_runner/__init__.py | 29 - .../pyodide_test_runner/browser.py | 546 ------------------ .../pyodide_test_runner/decorator.py | 300 ---------- .../pyodide_test_runner/fixture.py | 239 -------- .../pyodide_test_runner/hook.py | 91 --- .../pyodide_test_runner/hypothesis.py | 60 -- .../pyodide_test_runner/node_test_driver.js | 84 --- .../pyodide_test_runner/server.py | 82 --- .../tests/test_decorator.py | 198 ------- .../tests/test_jsassert.py | 66 --- .../pyodide_test_runner/tests/test_testing.py | 7 - .../pyodide_test_runner/utils.py | 102 ---- pyodide-test-runner/pyproject.toml | 4 - pyodide-test-runner/setup.cfg | 49 -- setup.cfg | 1 - src/tests/test_browser_apis.py | 2 +- src/tests/test_bz2.py | 2 +- src/tests/test_console.py | 2 +- src/tests/test_jsproxy.py | 2 +- src/tests/test_pyodide.py | 2 +- src/tests/test_pyodide_http.py | 2 +- src/tests/test_sqlite3.py | 2 +- src/tests/test_typeconversions.py | 6 +- tools/bump_version.py | 5 - 84 files changed, 99 insertions(+), 1982 deletions(-) delete mode 100644 pyodide-test-runner/README.md delete mode 100644 pyodide-test-runner/conftest.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/__init__.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/browser.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/decorator.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/fixture.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/hook.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/hypothesis.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/node_test_driver.js delete mode 100644 pyodide-test-runner/pyodide_test_runner/server.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/tests/test_decorator.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/tests/test_jsassert.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/tests/test_testing.py delete mode 100644 pyodide-test-runner/pyodide_test_runner/utils.py delete mode 100644 pyodide-test-runner/pyproject.toml delete mode 100644 pyodide-test-runner/setup.cfg diff --git a/.circleci/config.yml b/.circleci/config.yml index 33de0d78d..620bcfcc0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3804760a3..1fb8b09c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index f2978688d..0ccfe5a5b 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -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, diff --git a/conftest.py b/conftest.py index 08533395e..1fddcba88 100644 --- a/conftest.py +++ b/conftest.py @@ -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; diff --git a/docs/development/new-packages.md b/docs/development/new-packages.md index 2c11fc052..1a84e4250 100644 --- a/docs/development/new-packages.md +++ b/docs/development/new-packages.md @@ -147,7 +147,7 @@ The tests should go in one or more files like `test_.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=[""]) 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=["-tests", "pytest"]) def test_mytestname(selenium): diff --git a/docs/development/testing.md b/docs/development/testing.md index 305df3aa4..d4a7f448f 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -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"]) diff --git a/docs/project/changelog.md b/docs/project/changelog.md index f7752fc62..66405ec86 100644 --- a/docs/project/changelog.md +++ b/docs/project/changelog.md @@ -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 diff --git a/packages/Jinja2/test_jinja2.py b/packages/Jinja2/test_jinja2.py index 59102afe4..8b6a3fa06 100644 --- a/packages/Jinja2/test_jinja2.py +++ b/packages/Jinja2/test_jinja2.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["Jinja2"]) diff --git a/packages/Pillow/test_pillow.py b/packages/Pillow/test_pillow.py index 19d9a1e3b..09f06591f 100644 --- a/packages/Pillow/test_pillow.py +++ b/packages/Pillow/test_pillow.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide( diff --git a/packages/RobotRaconteur/test_robotraconteur_pyodide.py b/packages/RobotRaconteur/test_robotraconteur_pyodide.py index 2d9b82197..0bb983705 100644 --- a/packages/RobotRaconteur/test_robotraconteur_pyodide.py +++ b/packages/RobotRaconteur/test_robotraconteur_pyodide.py @@ -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"]) diff --git a/packages/astropy/test_astropy.py b/packages/astropy/test_astropy.py index 6c6234093..80e0a75f2 100644 --- a/packages/astropy/test_astropy.py +++ b/packages/astropy/test_astropy.py @@ -1,5 +1,5 @@ import pytest -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @pytest.mark.parametrize( diff --git a/packages/bitarray/test_bitarray.py b/packages/bitarray/test_bitarray.py index a739f48bf..88c5a3ad4 100644 --- a/packages/bitarray/test_bitarray.py +++ b/packages/bitarray/test_bitarray.py @@ -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"]) diff --git a/packages/boost-histogram/test_boost_histogram.py b/packages/boost-histogram/test_boost_histogram.py index b901f16ef..2ea25c4d6 100644 --- a/packages/boost-histogram/test_boost_histogram.py +++ b/packages/boost-histogram/test_boost_histogram.py @@ -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"]) diff --git a/packages/cffi/test_cffi.py b/packages/cffi/test_cffi.py index dfb5afc55..e6ea588b8 100644 --- a/packages/cffi/test_cffi.py +++ b/packages/cffi/test_cffi.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["cffi"]) diff --git a/packages/cffi_example/test_cffi_example.py b/packages/cffi_example/test_cffi_example.py index 8632df457..7ee161a6f 100644 --- a/packages/cffi_example/test_cffi_example.py +++ b/packages/cffi_example/test_cffi_example.py @@ -1,5 +1,5 @@ import pytest -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @pytest.mark.parametrize( diff --git a/packages/cloudpickle/test_cloudpickle.py b/packages/cloudpickle/test_cloudpickle.py index 793587377..2932d928a 100644 --- a/packages/cloudpickle/test_cloudpickle.py +++ b/packages/cloudpickle/test_cloudpickle.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["cloudpickle"]) diff --git a/packages/cryptography/test_cryptography.py b/packages/cryptography/test_cryptography.py index 2caa35272..dabf50fe7 100644 --- a/packages/cryptography/test_cryptography.py +++ b/packages/cryptography/test_cryptography.py @@ -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"]) diff --git a/packages/fpcast-test/test_fpcast_test.py b/packages/fpcast-test/test_fpcast_test.py index c2e84f7a5..ea0335a42 100644 --- a/packages/fpcast-test/test_fpcast_test.py +++ b/packages/fpcast-test/test_fpcast_test.py @@ -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"]) diff --git a/packages/galpy/test_galpy.py b/packages/galpy/test_galpy.py index a690dc40f..57c521e7c 100644 --- a/packages/galpy/test_galpy.py +++ b/packages/galpy/test_galpy.py @@ -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 = [ diff --git a/packages/gmpy2/test_gmpy2.py b/packages/gmpy2/test_gmpy2.py index 2b7f99dcd..6468247f2 100644 --- a/packages/gmpy2/test_gmpy2.py +++ b/packages/gmpy2/test_gmpy2.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["gmpy2"]) diff --git a/packages/imageio/test_imageio.py b/packages/imageio/test_imageio.py index e9131b802..6a7b4ca2b 100644 --- a/packages/imageio/test_imageio.py +++ b/packages/imageio/test_imageio.py @@ -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"]) diff --git a/packages/jedi/test_jedi.py b/packages/jedi/test_jedi.py index 12d004311..98eb86a95 100644 --- a/packages/jedi/test_jedi.py +++ b/packages/jedi/test_jedi.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["jedi"]) diff --git a/packages/lazy-object-proxy/test_lazy_object_proxy.py b/packages/lazy-object-proxy/test_lazy_object_proxy.py index 03789962c..a369167a8 100644 --- a/packages/lazy-object-proxy/test_lazy_object_proxy.py +++ b/packages/lazy-object-proxy/test_lazy_object_proxy.py @@ -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"]) diff --git a/packages/lxml/test_lxml.py b/packages/lxml/test_lxml.py index 02e3e0ceb..0418d3c86 100644 --- a/packages/lxml/test_lxml.py +++ b/packages/lxml/test_lxml.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["lxml"]) diff --git a/packages/micropip/test_micropip.py b/packages/micropip/test_micropip.py index 8c641d9e7..733741858 100644 --- a/packages/micropip/test_micropip.py +++ b/packages/micropip/test_micropip.py @@ -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")) diff --git a/packages/msgpack/test_pack.py b/packages/msgpack/test_pack.py index 914dfa69f..6a65059f2 100644 --- a/packages/msgpack/test_pack.py +++ b/packages/msgpack/test_pack.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["msgpack"]) diff --git a/packages/msprime/test_msprime.py b/packages/msprime/test_msprime.py index 5045d84ec..8f5582df0 100644 --- a/packages/msprime/test_msprime.py +++ b/packages/msprime/test_msprime.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide( diff --git a/packages/networkx/test_networkx.py b/packages/networkx/test_networkx.py index 1571434db..38b3d7c97 100644 --- a/packages/networkx/test_networkx.py +++ b/packages/networkx/test_networkx.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["networkx"]) diff --git a/packages/nlopt/test_nlopt.py b/packages/nlopt/test_nlopt.py index 1eb893b28..8c463cd08 100644 --- a/packages/nlopt/test_nlopt.py +++ b/packages/nlopt/test_nlopt.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide( diff --git a/packages/numcodecs/test_numcodecs.py b/packages/numcodecs/test_numcodecs.py index 05d3737c7..628ad7200 100644 --- a/packages/numcodecs/test_numcodecs.py +++ b/packages/numcodecs/test_numcodecs.py @@ -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( diff --git a/packages/numpy/test_numpy.py b/packages/numpy/test_numpy.py index eb10a1bf0..78ac34bb3 100644 --- a/packages/numpy/test_numpy.py +++ b/packages/numpy/test_numpy.py @@ -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): diff --git a/packages/opencv-python/test_opencv_python.py b/packages/opencv-python/test_opencv_python.py index e0fe5b866..7c9fe495a 100644 --- a/packages/opencv-python/test_opencv_python.py +++ b/packages/opencv-python/test_opencv_python.py @@ -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" diff --git a/packages/pyclipper/test_pyclipper.py b/packages/pyclipper/test_pyclipper.py index c4f5e7626..9884f84c9 100644 --- a/packages/pyclipper/test_pyclipper.py +++ b/packages/pyclipper/test_pyclipper.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["pyclipper"]) diff --git a/packages/pyproj/test_pyproj.py b/packages/pyproj/test_pyproj.py index 22c991712..c0972e396 100644 --- a/packages/pyproj/test_pyproj.py +++ b/packages/pyproj/test_pyproj.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["pyproj"]) diff --git a/packages/python-sat/test_python_sat.py b/packages/python-sat/test_python_sat.py index f96cba146..8c1825dc7 100644 --- a/packages/python-sat/test_python_sat.py +++ b/packages/python-sat/test_python_sat.py @@ -1,5 +1,5 @@ import pytest -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide solvers = [ "cadical", diff --git a/packages/python_solvespace/test_python_solvespace.py b/packages/python_solvespace/test_python_solvespace.py index 11b187bf9..fbd9ba5cc 100644 --- a/packages/python_solvespace/test_python_solvespace.py +++ b/packages/python_solvespace/test_python_solvespace.py @@ -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"]) diff --git a/packages/pywavelets/test_pywt.py b/packages/pywavelets/test_pywt.py index 4c1e91462..6f21897f2 100644 --- a/packages/pywavelets/test_pywt.py +++ b/packages/pywavelets/test_pywt.py @@ -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) diff --git a/packages/pyyaml/test_pyyaml.py b/packages/pyyaml/test_pyyaml.py index 403e9f5eb..b7ff44705 100644 --- a/packages/pyyaml/test_pyyaml.py +++ b/packages/pyyaml/test_pyyaml.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["pyyaml"]) diff --git a/packages/rebound/test_rebound.py b/packages/rebound/test_rebound.py index 0040bdb5e..93bef0e26 100644 --- a/packages/rebound/test_rebound.py +++ b/packages/rebound/test_rebound.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide( diff --git a/packages/regex/test_regex.py b/packages/regex/test_regex.py index 89b93e91b..9909f4e20 100644 --- a/packages/regex/test_regex.py +++ b/packages/regex/test_regex.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["regex"]) diff --git a/packages/scikit-image/test_skimage.py b/packages/scikit-image/test_skimage.py index 05c60b002..5ccdd3246 100644 --- a/packages/scikit-image/test_skimage.py +++ b/packages/scikit-image/test_skimage.py @@ -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( diff --git a/packages/scikit-learn/test_scikit-learn.py b/packages/scikit-learn/test_scikit-learn.py index 508d95ae8..f3ab0b18b 100644 --- a/packages/scikit-learn/test_scikit-learn.py +++ b/packages/scikit-learn/test_scikit-learn.py @@ -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) diff --git a/packages/scipy/test_scipy.py b/packages/scipy/test_scipy.py index c39075a47..b6938fad2 100644 --- a/packages/scipy/test_scipy.py +++ b/packages/scipy/test_scipy.py @@ -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) diff --git a/packages/shapely/test_shapely.py b/packages/shapely/test_shapely.py index da95d017d..d1d3f5e18 100644 --- a/packages/shapely/test_shapely.py +++ b/packages/shapely/test_shapely.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["shapely"]) diff --git a/packages/sharedlib-test-py/test_sharedlib.py b/packages/sharedlib-test-py/test_sharedlib.py index c489d63b5..2e6b0915d 100644 --- a/packages/sharedlib-test-py/test_sharedlib.py +++ b/packages/sharedlib-test-py/test_sharedlib.py @@ -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"]) diff --git a/packages/sparseqr/test_sparseqr.py b/packages/sparseqr/test_sparseqr.py index 75f822933..1208b9f9f 100644 --- a/packages/sparseqr/test_sparseqr.py +++ b/packages/sparseqr/test_sparseqr.py @@ -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) diff --git a/packages/sqlalchemy/test_sqlalchemy.py b/packages/sqlalchemy/test_sqlalchemy.py index da0ac80ca..ed5fe3b32 100644 --- a/packages/sqlalchemy/test_sqlalchemy.py +++ b/packages/sqlalchemy/test_sqlalchemy.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["sqlalchemy"]) diff --git a/packages/ssl/test_ssl.py b/packages/ssl/test_ssl.py index e1f9fee58..f3c72b3ea 100644 --- a/packages/ssl/test_ssl.py +++ b/packages/ssl/test_ssl.py @@ -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) diff --git a/packages/sympy/test_sympy.py b/packages/sympy/test_sympy.py index 8ab9169b9..7f1b406b3 100644 --- a/packages/sympy/test_sympy.py +++ b/packages/sympy/test_sympy.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["sympy"]) diff --git a/packages/termcolor/test_termcolor.py b/packages/termcolor/test_termcolor.py index 3c27dd9c7..c00977b4f 100644 --- a/packages/termcolor/test_termcolor.py +++ b/packages/termcolor/test_termcolor.py @@ -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"]) diff --git a/packages/test_packages_common.py b/packages/test_packages_common.py index e4225d088..2af86a85e 100644 --- a/packages/test_packages_common.py +++ b/packages/test_packages_common.py @@ -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 diff --git a/packages/tskit/test_tskit.py b/packages/tskit/test_tskit.py index 29b6cc84b..e9d97e54d 100644 --- a/packages/tskit/test_tskit.py +++ b/packages/tskit/test_tskit.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide( diff --git a/packages/wrapt/test_wrapt.py b/packages/wrapt/test_wrapt.py index b3f597d28..d57c0eb0e 100644 --- a/packages/wrapt/test_wrapt.py +++ b/packages/wrapt/test_wrapt.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["wrapt"]) diff --git a/packages/xarray/test_xarray.py b/packages/xarray/test_xarray.py index 9be60ee01..183611aed 100644 --- a/packages/xarray/test_xarray.py +++ b/packages/xarray/test_xarray.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide(packages=["xarray"]) diff --git a/packages/xgboost/test_xgboost.py b/packages/xgboost/test_xgboost.py index b43f1bd5c..e59744edc 100644 --- a/packages/xgboost/test_xgboost.py +++ b/packages/xgboost/test_xgboost.py @@ -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()) diff --git a/packages/zarr/test_zarr.py b/packages/zarr/test_zarr.py index b252fe6a4..5b7dada5d 100644 --- a/packages/zarr/test_zarr.py +++ b/packages/zarr/test_zarr.py @@ -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"]) diff --git a/pyodide-build/pyodide_build/tests/test_pywasmcross.py b/pyodide-build/pyodide_build/tests/test_pywasmcross.py index d9ad629eb..5e57e5d00 100644 --- a/pyodide-build/pyodide_build/tests/test_pywasmcross.py +++ b/pyodide-build/pyodide_build/tests/test_pywasmcross.py @@ -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(); diff --git a/pyodide-build/setup.cfg b/pyodide-build/setup.cfg index 19098e04b..45b181522 100644 --- a/pyodide-build/setup.cfg +++ b/pyodide-build/setup.cfg @@ -35,6 +35,7 @@ console_scripts = [options.extras_require] test = pytest + pytest-pyodide [options.packages.find] where = . diff --git a/pyodide-test-runner/README.md b/pyodide-test-runner/README.md deleted file mode 100644 index 61773730b..000000000 --- a/pyodide-test-runner/README.md +++ /dev/null @@ -1 +0,0 @@ -# pyodide-test-runner diff --git a/pyodide-test-runner/conftest.py b/pyodide-test-runner/conftest.py deleted file mode 100644 index 90a2f65df..000000000 --- a/pyodide-test-runner/conftest.py +++ /dev/null @@ -1,4 +0,0 @@ -pytest_plugins = [ - "pyodide_test_runner.hook", - "pyodide_test_runner.fixture", -] diff --git a/pyodide-test-runner/pyodide_test_runner/__init__.py b/pyodide-test-runner/pyodide_test_runner/__init__.py deleted file mode 100644 index bd55fff07..000000000 --- a/pyodide-test-runner/pyodide_test_runner/__init__.py +++ /dev/null @@ -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", -] diff --git a/pyodide-test-runner/pyodide_test_runner/browser.py b/pyodide-test-runner/pyodide_test_runner/browser.py deleted file mode 100644 index 58363b048..000000000 --- a/pyodide-test-runner/pyodide_test_runner/browser.py +++ /dev/null @@ -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()) diff --git a/pyodide-test-runner/pyodide_test_runner/decorator.py b/pyodide-test-runner/pyodide_test_runner/decorator.py deleted file mode 100644 index fab18f4c1..000000000 --- a/pyodide-test-runner/pyodide_test_runner/decorator.py +++ /dev/null @@ -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 - def func(, arg1, arg2, arg3): - # do stuff - - This wrapper looks like: - - def (, arg1, arg2, arg3): - run_test(, (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. - # 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 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 diff --git a/pyodide-test-runner/pyodide_test_runner/fixture.py b/pyodide-test-runner/pyodide_test_runner/fixture.py deleted file mode 100644 index 8abae716b..000000000 --- a/pyodide-test-runner/pyodide_test_runner/fixture.py +++ /dev/null @@ -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 diff --git a/pyodide-test-runner/pyodide_test_runner/hook.py b/pyodide-test-runner/pyodide_test_runner/hook.py deleted file mode 100644 index 78102258f..000000000 --- a/pyodide-test-runner/pyodide_test_runner/hook.py +++ /dev/null @@ -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) diff --git a/pyodide-test-runner/pyodide_test_runner/hypothesis.py b/pyodide-test-runner/pyodide_test_runner/hypothesis.py deleted file mode 100644 index 8725ed605..000000000 --- a/pyodide-test-runner/pyodide_test_runner/hypothesis.py +++ /dev/null @@ -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) -) diff --git a/pyodide-test-runner/pyodide_test_runner/node_test_driver.js b/pyodide-test-runner/pyodide_test_runner/node_test_driver.js deleted file mode 100644 index 10b899103..000000000 --- a/pyodide-test-runner/pyodide_test_runner/node_test_driver.js +++ /dev/null @@ -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); diff --git a/pyodide-test-runner/pyodide_test_runner/server.py b/pyodide-test-runner/pyodide_test_runner/server.py deleted file mode 100644 index d34cf8393..000000000 --- a/pyodide-test-runner/pyodide_test_runner/server.py +++ /dev/null @@ -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() diff --git a/pyodide-test-runner/pyodide_test_runner/tests/test_decorator.py b/pyodide-test-runner/pyodide_test_runner/tests/test_decorator.py deleted file mode 100644 index 0b4f417ae..000000000 --- a/pyodide-test-runner/pyodide_test_runner/tests/test_decorator.py +++ /dev/null @@ -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 diff --git a/pyodide-test-runner/pyodide_test_runner/tests/test_jsassert.py b/pyodide-test-runner/pyodide_test_runner/tests/test_jsassert.py deleted file mode 100644 index 6d7a94b6d..000000000 --- a/pyodide-test-runner/pyodide_test_runner/tests/test_jsassert.py +++ /dev/null @@ -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); - """ - ) diff --git a/pyodide-test-runner/pyodide_test_runner/tests/test_testing.py b/pyodide-test-runner/pyodide_test_runner/tests/test_testing.py deleted file mode 100644 index 7f8fca304..000000000 --- a/pyodide-test-runner/pyodide_test_runner/tests/test_testing.py +++ /dev/null @@ -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 diff --git a/pyodide-test-runner/pyodide_test_runner/utils.py b/pyodide-test-runner/pyodide_test_runner/utils.py deleted file mode 100644 index 7da9bcb8f..000000000 --- a/pyodide-test-runner/pyodide_test_runner/utils.py +++ /dev/null @@ -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[\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[\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) diff --git a/pyodide-test-runner/pyproject.toml b/pyodide-test-runner/pyproject.toml deleted file mode 100644 index 1ce9aa496..000000000 --- a/pyodide-test-runner/pyproject.toml +++ /dev/null @@ -1,4 +0,0 @@ -[build-system] -requires = ["setuptools>=42", "wheel"] - -build-backend = "setuptools.build_meta" diff --git a/pyodide-test-runner/setup.cfg b/pyodide-test-runner/setup.cfg deleted file mode 100644 index a20a85749..000000000 --- a/pyodide-test-runner/setup.cfg +++ /dev/null @@ -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 diff --git a/setup.cfg b/setup.cfg index 2615ae81a..feeb426db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,6 @@ norecursedirs = addopts = --doctest-modules --ignore="packages/matplotlib/src" - --ignore="pyodide-test-runner" --tb=short --dist-dir=dist diff --git a/src/tests/test_browser_apis.py b/src/tests/test_browser_apis.py index 7c069a833..40e0b79f9 100644 --- a/src/tests/test_browser_apis.py +++ b/src/tests/test_browser_apis.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide diff --git a/src/tests/test_bz2.py b/src/tests/test_bz2.py index 1d46fcb6d..b09ba47f5 100644 --- a/src/tests/test_bz2.py +++ b/src/tests/test_bz2.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide diff --git a/src/tests/test_console.py b/src/tests/test_console.py index 38159cdec..11600cccc 100644 --- a/src/tests/test_console.py +++ b/src/tests/test_console.py @@ -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 diff --git a/src/tests/test_jsproxy.py b/src/tests/test_jsproxy.py index a318fca6b..36828456f 100644 --- a/src/tests/test_jsproxy.py +++ b/src/tests/test_jsproxy.py @@ -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): diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py index ef0268065..cf5df80ca 100644 --- a/src/tests/test_pyodide.py +++ b/src/tests/test_pyodide.py @@ -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 diff --git a/src/tests/test_pyodide_http.py b/src/tests/test_pyodide_http.py index 1250bd72e..9ebc7a086 100644 --- a/src/tests/test_pyodide_http.py +++ b/src/tests/test_pyodide_http.py @@ -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") diff --git a/src/tests/test_sqlite3.py b/src/tests/test_sqlite3.py index 58064f483..c0516e113 100644 --- a/src/tests/test_sqlite3.py +++ b/src/tests/test_sqlite3.py @@ -1,4 +1,4 @@ -from pyodide_test_runner import run_in_pyodide +from pytest_pyodide import run_in_pyodide @run_in_pyodide diff --git a/src/tests/test_typeconversions.py b/src/tests/test_typeconversions.py index e0dd02358..03a8e4f48 100644 --- a/src/tests/test_typeconversions.py +++ b/src/tests/test_typeconversions.py @@ -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, diff --git a/tools/bump_version.py b/tools/bump_version.py index 31ffd1cf0..fbab4253c 100755 --- a/tools/bump_version.py +++ b/tools/bump_version.py @@ -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 = [