diff --git a/.github/workflows/build_pyodide.yml b/.github/workflows/build_pyodide.yml index b69c1db125..8318460149 100644 --- a/.github/workflows/build_pyodide.yml +++ b/.github/workflows/build_pyodide.yml @@ -84,16 +84,13 @@ jobs: - name: Run wheel tests run: | - cp -r IfcOpenShell/pyodide/test test # venv set up in build_pyodide.sh. source .venv/bin/activate - uv pip install pytest-pyodide - PYODIDE_ROOT_DIST=`pyodide config get pyodide_root`/dist - # `pytest-pyodide` requires pyodide in 'pyodide' directory in cwd, when running `pytest`. - cp -r $PYODIDE_ROOT_DIST test/pyodide - cp dist/ifcopenshell-*.whl test/pyodide - cd test - pytest --capture=no + ln -s "$PWD/dist" IfcOpenShell/dist + ln -s "$PWD/dist-modular" IfcOpenShell/dist-modular + cd IfcOpenShell/pyodide + ./run_pytest.py setup + ./run_pytest.py run - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v6 diff --git a/.gitignore b/.gitignore index addd55528c..51ac937751 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ /src/ifcmax/out/ /src/ifcwrap/out/ /src/ifctester/webapp/public/pyodide/ +# pyodide wheels +/dist/ +/dist-modular/ /win/BuildDepsCache*.txt diff --git a/pyodide/README.md b/pyodide/README.md index 0bab71fa30..c7ce24321d 100644 --- a/pyodide/README.md +++ b/pyodide/README.md @@ -30,3 +30,8 @@ since it's pure cmake without any additional moving parts. - it will produce Python package in `IfcOpenShell/ifcopenshell` - run `pyodide build` - it will produce a wheel in `IfcOpenShell/dist` +- testing: + - ensure you're in pyodide environment + - `cd IfcOpenshell/pyodide` + - `./run_pytest.py setup` + - `./run_pytest.py run` diff --git a/pyodide/__init__.py b/pyodide/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyodide/run_pytest.py b/pyodide/run_pytest.py new file mode 100755 index 0000000000..828e2b1cd0 --- /dev/null +++ b/pyodide/run_pytest.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +import argparse +import shlex +import shutil +import subprocess +from pathlib import Path + +SCRIPT_DIR = Path(__file__).parent + +DIST_DIRS = ( + SCRIPT_DIR / "test/pyodide", + SCRIPT_DIR / "test/pyodide-modular", +) +WHEEL_SRCS = ( + SCRIPT_DIR / "../dist", + SCRIPT_DIR / "../dist-modular", +) + + +def run(cmd: list, **kwargs) -> None: + print("$", shlex.join(str(part) for part in cmd)) + subprocess.check_call(cmd, **kwargs) + + +def setup() -> None: + run(["uv", "pip", "install", "pytest-pyodide"]) + + # Copy pyodide installation so we can modify it locally just for tests. + pyodide_root = subprocess.check_output(["pyodide", "config", "get", "pyodide_root"], text=True).strip() + pyodide_root_dist = Path(pyodide_root) / "dist" + for dist_dir in DIST_DIRS: + if dist_dir.exists(): + shutil.rmtree(dist_dir) + shutil.copytree(pyodide_root_dist, dist_dir) + + +def run_tests() -> None: + for dist_dir, wheel_src in zip(DIST_DIRS, WHEEL_SRCS): + if not wheel_src.exists(): + raise RuntimeError(f"error: {wheel_src} does not exist") + + # Clean up previous wheels. + for whl in dist_dir.glob("ifcopenshell*.whl"): + whl.unlink() + + # Symlink new ones. + for whl in wheel_src.glob("ifcopenshell*.whl"): + (dist_dir / whl.name).symlink_to(whl.resolve()) + + for dist_dir in DIST_DIRS: + run(["pytest", f"--dist-dir={dist_dir}", "--capture=no"], cwd=SCRIPT_DIR) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("command", choices=["setup", "run"]) + args = parser.parse_args() + + if args.command == "setup": + setup() + else: + run_tests() diff --git a/pyodide/test/.gitignore b/pyodide/test/.gitignore new file mode 100644 index 0000000000..e2b605aed2 --- /dev/null +++ b/pyodide/test/.gitignore @@ -0,0 +1,2 @@ +pyodide +pyodide-modular diff --git a/pyodide/test/__init__.py b/pyodide/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyodide/test/test_wheel.py b/pyodide/test/test_wheel.py index 2ef5e0e193..a23b72c493 100644 --- a/pyodide/test/test_wheel.py +++ b/pyodide/test/test_wheel.py @@ -1,18 +1,30 @@ +import zipfile from pathlib import Path -WHEEL_FILENAME = next( - p.name for p in (Path.cwd() / "pyodide").iterdir() if p.name.startswith("ifcopenshell-") and p.suffix == ".whl" -) +from ..order_pyodide_wheel_shared_objects import shared_object_sort_key -def test_ifcopenshell_import(selenium): +def _first_so_name(wheel_path: Path) -> str: + with zipfile.ZipFile(wheel_path) as zf: + for name in zf.namelist(): + if name.endswith(".so"): + return Path(name).name + return wheel_path.name + + +def test_ifcopenshell_import(selenium, request): + dist_dir = Path(request.config.getoption("--dist-dir")) + wheel_paths = list(dist_dir.glob("ifcopenshell*.whl")) + wheel_paths.sort(key=lambda path: shared_object_sort_key(_first_so_name(path), 0)) + WHEEL_NAMES = tuple(path.name for path in wheel_paths) + selenium.load_package("micropip") - # Important to test it with `micropip.install` - # without any dependencies loaded to ensure micropip will load them automatically. - selenium.run_async( - f""" + selenium.run_async(f""" import micropip - await micropip.install(f"./{WHEEL_FILENAME}") + wheel_filenames = {WHEEL_NAMES!r} + for wheel_filename in wheel_filenames: + print(f"Loading {{wheel_filename}}...") + await micropip.install(f"./{{wheel_filename}}") import ifcopenshell from pathlib import Path ifcopenshell.set_plugin_search_paths([str(Path(ifcopenshell.__file__).parent)]) @@ -24,5 +36,4 @@ def test_ifcopenshell_import(selenium): wall.Name = "Test" assert wall.Name == "Test", f"Entity name wasn't changed: {{wall}}" print(wall) - """ - ) + """) diff --git a/pyproject.toml b/pyproject.toml index 519ef3b73e..2bd6b6c3ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ line-length = 120 include = ''' src/.*.pyi?$ |nix/.*.pyi?$ + |pyodide/.*.pyi?$ ''' extend-exclude = ''' src/ifcopenshell-python/ifcopenshell/express/rules/*