From 1695ddeb640d3617de9cc21485451b1d0784406d Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 15 May 2023 16:53:57 -0400 Subject: [PATCH 1/3] tests: run on pyodide Signed-off-by: Henry Schreiner --- .github/workflows/emscripten.yaml | 58 ++++++++++++++++++++++++++++ tests/CMakeLists.txt | 10 ++++- tests/pyproject.toml | 11 ++++++ tests/test_async.py | 5 +++ tests/test_callbacks.py | 3 ++ tests/test_embed/test_interpreter.py | 7 ++++ tests/test_embed/test_trampoline.py | 9 +++++ tests/test_exceptions.py | 2 +- tests/test_gil_scoped.py | 13 +++++-- tests/test_iostream.py | 4 ++ tests/test_thread.py | 5 +++ tests/test_virtual_functions.py | 3 ++ 12 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/emscripten.yaml create mode 100644 tests/pyproject.toml diff --git a/.github/workflows/emscripten.yaml b/.github/workflows/emscripten.yaml new file mode 100644 index 0000000000..9a84d3e12a --- /dev/null +++ b/.github/workflows/emscripten.yaml @@ -0,0 +1,58 @@ +name: WASM + +on: + workflow_dispatch: + pull_request: + branches: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-wasm-emscripten: + name: Pyodide wheel + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install pyodide-build + run: pip install pyodide-build==0.23.4 + + - name: Compute emsdk version + id: compute-emsdk-version + run: | + # Prepare xbuild environment (side-effect) + pyodide config list + # Save EMSDK version + EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version) + echo "emsdk-version=$EMSCRIPTEN_VERSION" >> $GITHUB_OUTPUT + + - uses: mymindstorm/setup-emsdk@v12 + with: + version: ${{ steps.compute-emsdk-version.outputs.emsdk-version }} + actions-cache-folder: emsdk-cache + + - name: Build + run: PYODIDE_BUILD_EXPORTS=whole_archive CFLAGS=-fexceptions LDFLAGS=-fexceptions pyodide build + working-directory: tests + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Set up Pyodide virtual environment + run: | + pyodide venv .venv-pyodide + .venv-pyodide/bin/pip install $(echo -n tests/dist/*.whl) + + - name: Test + run: .venv-pyodide/bin/pytest -o timeout=0 tests/test_*.py diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f182e24992..aae9be720b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -88,7 +88,12 @@ set(PYBIND11_TEST_FILTER if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) # We're being loaded directly, i.e. not via add_subdirectory, so make this # work as its own project and load the pybind11Config to get the tools we need - find_package(pybind11 REQUIRED CONFIG) + + if(SKBUILD) + add_subdirectory(.. pybind11_src) + else() + find_package(pybind11 REQUIRED CONFIG) + endif() endif() if(NOT CMAKE_BUILD_TYPE AND NOT DEFINED CMAKE_CONFIGURATION_TYPES) @@ -489,6 +494,9 @@ foreach(target ${test_targets}) endforeach() endif() endif() + if(SKBUILD) + install(TARGETS ${target} LIBRARY DESTINATION .) + endif() endforeach() # Provide nice organisation in IDEs diff --git a/tests/pyproject.toml b/tests/pyproject.toml new file mode 100644 index 0000000000..97478b0b82 --- /dev/null +++ b/tests/pyproject.toml @@ -0,0 +1,11 @@ +# Warning: this is currently used for pyodide, and is not a general out-of-tree +# builder for the tests (yet). Specifically, wheels can't be built from SDists. + +[build-system] +requires = ["scikit-build-core[pyproject]"] +build-backend = "scikit_build_core.build" + +[project] +name = "pybind11_tests" +version = "0.0.1" +dependencies = ["pytest", "pytest-timeout", "numpy", "scipy"] diff --git a/tests/test_async.py b/tests/test_async.py index 4d33ba65f6..1705196d14 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -1,10 +1,15 @@ from __future__ import annotations +import sys + import pytest asyncio = pytest.importorskip("asyncio") m = pytest.importorskip("pybind11_tests.async_module") +if sys.platform.startswith("emscripten"): + pytest.skip("Can't run a new event_loop in pyodide", allow_module_level=True) + @pytest.fixture() def event_loop(): diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index ce2a6d2549..db6d8dece0 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys import time from threading import Thread @@ -153,6 +154,7 @@ def test_python_builtins(): assert m.test_sum_builtin(sum, []) == 0 +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_async_callbacks(): # serves as state for async callback class Item: @@ -176,6 +178,7 @@ def gen_f(): assert sum(res) == sum(x + 3 for x in work) +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_async_async_callbacks(): t = Thread(target=test_async_callbacks) t.start() diff --git a/tests/test_embed/test_interpreter.py b/tests/test_embed/test_interpreter.py index 424d36dea2..7003fcff76 100644 --- a/tests/test_embed/test_interpreter.py +++ b/tests/test_embed/test_interpreter.py @@ -2,6 +2,13 @@ import sys +import pytest + +if sys.platform.startswith("emscripten"): + pytest.skip( + "Test not implemented from single wheel on Pyodide", allow_module_level=True + ) + from widget_module import Widget diff --git a/tests/test_embed/test_trampoline.py b/tests/test_embed/test_trampoline.py index b8ff3eba28..011529961d 100644 --- a/tests/test_embed/test_trampoline.py +++ b/tests/test_embed/test_trampoline.py @@ -1,5 +1,14 @@ from __future__ import annotations +import sys + +import pytest + +if sys.platform.startswith("emscripten"): + pytest.skip( + "Test not implemented from single wheel on Pyodide", allow_module_level=True + ) + import trampoline_module diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b33997eeea..db2a551e45 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -75,7 +75,7 @@ def test_cross_module_exceptions(msg): # TODO: FIXME @pytest.mark.xfail( - "env.MACOS and (env.PYPY or pybind11_tests.compiler_info.startswith('Homebrew Clang'))", + "env.MACOS and (env.PYPY or pybind11_tests.compiler_info.startswith('Homebrew Clang')) or sys.platform.startswith('emscripten')", raises=RuntimeError, reason="See Issue #2847, PR #2999, PR #4324", ) diff --git a/tests/test_gil_scoped.py b/tests/test_gil_scoped.py index a183876845..eab92093c8 100644 --- a/tests/test_gil_scoped.py +++ b/tests/test_gil_scoped.py @@ -71,24 +71,28 @@ def test_cross_module_gil_inner_pybind11_acquired(): m.test_cross_module_gil_inner_pybind11_acquired() +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_cross_module_gil_nested_custom_released(): """Makes sure that the GIL can be nested acquired/released by another module from a GIL-released state using custom locking logic.""" m.test_cross_module_gil_nested_custom_released() +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_cross_module_gil_nested_custom_acquired(): """Makes sure that the GIL can be nested acquired/acquired by another module from a GIL-acquired state using custom locking logic.""" m.test_cross_module_gil_nested_custom_acquired() +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_cross_module_gil_nested_pybind11_released(): """Makes sure that the GIL can be nested acquired/released by another module from a GIL-released state using pybind11 locking logic.""" m.test_cross_module_gil_nested_pybind11_released() +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_cross_module_gil_nested_pybind11_acquired(): """Makes sure that the GIL can be nested acquired/acquired by another module from a GIL-acquired state using pybind11 locking logic.""" @@ -103,6 +107,7 @@ def test_nested_acquire(): assert m.test_nested_acquire(0xAB) == "171" +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_multi_acquire_release_cross_module(): for bits in range(16 * 8): internals_ids = m.test_multi_acquire_release_cross_module(bits) @@ -204,7 +209,7 @@ def _run_in_threads(test_fn, num_threads, parallel): thread.join() -# TODO: FIXME, sometimes returns -11 (segfault) instead of 0 on macOS Python 3.9 +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") @pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK) def test_run_in_process_one_thread(test_fn): """Makes sure there is no GIL deadlock when running in a thread. @@ -214,7 +219,7 @@ def test_run_in_process_one_thread(test_fn): assert _run_in_process(_run_in_threads, test_fn, num_threads=1, parallel=False) == 0 -# TODO: FIXME on macOS Python 3.9 +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") @pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK) def test_run_in_process_multiple_threads_parallel(test_fn): """Makes sure there is no GIL deadlock when running in a thread multiple times in parallel. @@ -224,7 +229,7 @@ def test_run_in_process_multiple_threads_parallel(test_fn): assert _run_in_process(_run_in_threads, test_fn, num_threads=8, parallel=True) == 0 -# TODO: FIXME on macOS Python 3.9 +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") @pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK) def test_run_in_process_multiple_threads_sequential(test_fn): """Makes sure there is no GIL deadlock when running in a thread multiple times sequentially. @@ -234,7 +239,7 @@ def test_run_in_process_multiple_threads_sequential(test_fn): assert _run_in_process(_run_in_threads, test_fn, num_threads=8, parallel=False) == 0 -# TODO: FIXME on macOS Python 3.9 +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") @pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK) def test_run_in_process_direct(test_fn): """Makes sure there is no GIL deadlock when using processes. diff --git a/tests/test_iostream.py b/tests/test_iostream.py index f7eeff502b..c3d987787a 100644 --- a/tests/test_iostream.py +++ b/tests/test_iostream.py @@ -1,8 +1,11 @@ from __future__ import annotations +import sys from contextlib import redirect_stderr, redirect_stdout from io import StringIO +import pytest + from pybind11_tests import iostream as m @@ -270,6 +273,7 @@ def test_redirect_both(capfd): assert stream2.getvalue() == msg2 +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_threading(): with m.ostream_redirect(stdout=True, stderr=False): # start some threads diff --git a/tests/test_thread.py b/tests/test_thread.py index 4541a305ec..f12020e41b 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -1,7 +1,10 @@ from __future__ import annotations +import sys import threading +import pytest + from pybind11_tests import thread as m @@ -24,6 +27,7 @@ def join(self): raise self.e +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_implicit_conversion(): a = Thread(m.test) b = Thread(m.test) @@ -34,6 +38,7 @@ def test_implicit_conversion(): x.join() +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_implicit_conversion_no_gil(): a = Thread(m.test_no_gil) b = Thread(m.test_no_gil) diff --git a/tests/test_virtual_functions.py b/tests/test_virtual_functions.py index 08acaa1908..bc0177787a 100644 --- a/tests/test_virtual_functions.py +++ b/tests/test_virtual_functions.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + import pytest import env # noqa: F401 @@ -435,6 +437,7 @@ def lucky_number(self): assert obj.say_everything() == "BT -7" +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_issue_1454(): # Fix issue #1454 (crash when acquiring/releasing GIL on another thread in Python 2.7) m.test_gil() From b218b343cd1be8b880c5df5023af555f7ed6ae89 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 26 Jun 2024 15:26:57 -0400 Subject: [PATCH 2/3] ci: use cibuildwheel for pyodide test Signed-off-by: Henry Schreiner --- .github/workflows/emscripten.yaml | 44 ++++++------------------------- tests/pyproject.toml | 8 +++++- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/.github/workflows/emscripten.yaml b/.github/workflows/emscripten.yaml index 9a84d3e12a..4f12e81c28 100644 --- a/.github/workflows/emscripten.yaml +++ b/.github/workflows/emscripten.yaml @@ -15,44 +15,16 @@ jobs: name: Pyodide wheel runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: pypa/cibuildwheel@v2.19 + env: + PYODIDE_BUILD_EXPORTS: whole_archive + CFLAGS: -fexceptions + LDFLAGS: -fexceptions with: - python-version: "3.11" - - - name: Install pyodide-build - run: pip install pyodide-build==0.23.4 - - - name: Compute emsdk version - id: compute-emsdk-version - run: | - # Prepare xbuild environment (side-effect) - pyodide config list - # Save EMSDK version - EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version) - echo "emsdk-version=$EMSCRIPTEN_VERSION" >> $GITHUB_OUTPUT - - - uses: mymindstorm/setup-emsdk@v12 - with: - version: ${{ steps.compute-emsdk-version.outputs.emsdk-version }} - actions-cache-folder: emsdk-cache - - - name: Build - run: PYODIDE_BUILD_EXPORTS=whole_archive CFLAGS=-fexceptions LDFLAGS=-fexceptions pyodide build - working-directory: tests - - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: Set up Pyodide virtual environment - run: | - pyodide venv .venv-pyodide - .venv-pyodide/bin/pip install $(echo -n tests/dist/*.whl) - - - name: Test - run: .venv-pyodide/bin/pytest -o timeout=0 tests/test_*.py + package-dir: tests + only: cp312-pyodide_wasm32 diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 97478b0b82..469c145dfd 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -2,10 +2,16 @@ # builder for the tests (yet). Specifically, wheels can't be built from SDists. [build-system] -requires = ["scikit-build-core[pyproject]"] +requires = ["scikit-build-core"] build-backend = "scikit_build_core.build" [project] name = "pybind11_tests" version = "0.0.1" dependencies = ["pytest", "pytest-timeout", "numpy", "scipy"] + +[tool.scikit-build.cmake.define] +PYBIND11_FINDPYTHON = true + +[tool.cibuildwheel] +test-command = "pytest -o timeout=0 -p no:cacheprovider {project}/tests/test_*.py" From c9a83e5216f038b166edce1ecce30acb048d33fb Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 26 Jun 2024 16:15:21 -0400 Subject: [PATCH 3/3] tests: revert changes to test_embed Signed-off-by: Henry Schreiner --- tests/test_embed/test_interpreter.py | 7 ------- tests/test_embed/test_trampoline.py | 9 --------- 2 files changed, 16 deletions(-) diff --git a/tests/test_embed/test_interpreter.py b/tests/test_embed/test_interpreter.py index 7003fcff76..424d36dea2 100644 --- a/tests/test_embed/test_interpreter.py +++ b/tests/test_embed/test_interpreter.py @@ -2,13 +2,6 @@ import sys -import pytest - -if sys.platform.startswith("emscripten"): - pytest.skip( - "Test not implemented from single wheel on Pyodide", allow_module_level=True - ) - from widget_module import Widget diff --git a/tests/test_embed/test_trampoline.py b/tests/test_embed/test_trampoline.py index 011529961d..b8ff3eba28 100644 --- a/tests/test_embed/test_trampoline.py +++ b/tests/test_embed/test_trampoline.py @@ -1,14 +1,5 @@ from __future__ import annotations -import sys - -import pytest - -if sys.platform.startswith("emscripten"): - pytest.skip( - "Test not implemented from single wheel on Pyodide", allow_module_level=True - ) - import trampoline_module