diff --git a/tests/conftest.py b/tests/conftest.py index 0900dc4..a95920c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,11 +3,15 @@ import io import sys import zipfile +from dataclasses import dataclass from importlib.metadata import Distribution, PackageNotFoundError from pathlib import Path from tempfile import TemporaryDirectory +from typing import Any import pytest +from packaging.utils import parse_wheel_filename +from pytest_httpserver import HTTPServer from pytest_pyodide import spawn_web_server @@ -56,16 +60,6 @@ def wheel_path(tmp_path_factory): yield output_dir -@pytest.fixture(scope="session") -def test_wheel_path(tmp_path_factory): - # Build a test wheel for testing - output_dir = tmp_path_factory.mktemp("wheel") - - _build(Path(__file__).parent / "test_data" / "test_wheel_uninstall", output_dir) - - yield output_dir - - @pytest.fixture def selenium_standalone_micropip(selenium_standalone, wheel_path): """Import micropip before entering test so that global initialization of @@ -93,6 +87,68 @@ def selenium_standalone_micropip(selenium_standalone, wheel_path): yield selenium_standalone +class WheelCatalog: + """ + A catalog of wheels for testing. + """ + + @dataclass + class Wheel: + _path: Path + + name: str + filename: str + top_level: str + url: str + + @property + def content(self) -> bytes: + return self._path.read_bytes() + + def __init__(self): + self._wheels = {} + + self._httpserver = HTTPServer() + self._httpserver.no_handler_status_code = 404 + + def __enter__(self): + self._httpserver.__enter__() + return self + + def __exit__(self, *args: Any): + self._httpserver.__exit__(*args) + + def _register_handler(self, path: Path) -> str: + self._httpserver.expect_request(f"/{path.name}").respond_with_data( + path.read_bytes(), + content_type="application/zip", + headers={"Access-Control-Allow-Origin": "*"}, + ) + + return self._httpserver.url_for(f"/{path.name}") + + def add_wheel(self, path: Path): + name = parse_wheel_filename(path.name)[0] + url = self._register_handler(path) + + self._wheels[name] = self.Wheel( + path, name, path.name, name.replace("-", "_"), url + ) + + def get(self, name: str) -> Wheel: + return self._wheels[name] + + +@pytest.fixture(scope="session") +def wheel_catalog(): + """Run a mock server that serves pre-built wheels""" + with WheelCatalog() as catalog: + for wheel in TEST_WHEEL_DIR.glob("*.whl"): + catalog.add_wheel(wheel) + + yield catalog + + @pytest.fixture def mock_platform(monkeypatch): monkeypatch.setenv("_PYTHON_HOST_PLATFORM", PLATFORM) diff --git a/tests/test_data/test_wheel_uninstall/README.md b/tests/test_data/test_wheel_uninstall/README.md new file mode 100644 index 0000000..28051a3 --- /dev/null +++ b/tests/test_data/test_wheel_uninstall/README.md @@ -0,0 +1,7 @@ +This directory contains a wheel that is used to test the uninstallation functionality of the micropip. +If something is changed in the wheel, create a new wheel and copy it to the `wheel` directory. + +```sh +python -m build +cp dist/test_wheel_uninstall-1.0.0-py3-none-any.whl ../wheel/ +``` \ No newline at end of file diff --git a/tests/test_data/wheel/test_wheel_uninstall-1.0.0-py3-none-any.whl b/tests/test_data/wheel/test_wheel_uninstall-1.0.0-py3-none-any.whl new file mode 100644 index 0000000..0b45f5a Binary files /dev/null and b/tests/test_data/wheel/test_wheel_uninstall-1.0.0-py3-none-any.whl differ diff --git a/tests/test_install.py b/tests/test_install.py index adcae86..9ddf77d 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1,7 +1,7 @@ import pytest -from conftest import SNOWBALL_WHEEL, TEST_WHEEL_DIR, mock_fetch_cls +from conftest import mock_fetch_cls from packaging.utils import parse_wheel_filename -from pytest_pyodide import run_in_pyodide, spawn_web_server +from pytest_pyodide import run_in_pyodide import micropip @@ -28,24 +28,21 @@ def test_install_simple(selenium_standalone_micropip): ) -@pytest.mark.parametrize("base_url", ["'{base_url}'", "'.'"]) -def test_install_custom_url(selenium_standalone_micropip, base_url): +def test_install_custom_url(selenium_standalone_micropip, wheel_catalog): selenium = selenium_standalone_micropip + snowball_wheel = wheel_catalog.get("snowballstemmer") + url = snowball_wheel.url - with spawn_web_server(TEST_WHEEL_DIR) as server: - server_hostname, server_port, _ = server - base_url = f"http://{server_hostname}:{server_port}/" - url = base_url + SNOWBALL_WHEEL + @run_in_pyodide + async def install_from_url(selenium, url): + import micropip - selenium.run_js( - f""" - await pyodide.runPythonAsync(` - import micropip - await micropip.install('{url}') - import snowballstemmer - `); - """ - ) + await micropip.install(url) + import snowballstemmer + + snowballstemmer.stemmer("english") + + install_from_url(selenium, url) @pytest.mark.xfail_browsers(chrome="node only", firefox="node only") @@ -313,59 +310,54 @@ async def test_load_binary_wheel2(selenium): import regex # noqa: F401 -def test_emfs(selenium_standalone_micropip): - with spawn_web_server(TEST_WHEEL_DIR) as server: - server_hostname, server_port, _ = server - url = f"http://{server_hostname}:{server_port}/" +def test_emfs(selenium_standalone_micropip, wheel_catalog): + snowball_wheel = wheel_catalog.get("snowballstemmer") - @run_in_pyodide(packages=["micropip"]) - async def run_test(selenium, url, wheel_name): - from pyodide.http import pyfetch + @run_in_pyodide() + async def run_test(selenium, url, wheel_name): + from pyodide.http import pyfetch - import micropip + import micropip - resp = await pyfetch(url + wheel_name) - await resp._into_file(open(wheel_name, "wb")) - await micropip.install("emfs:" + wheel_name) - import snowballstemmer + resp = await pyfetch(url) + await resp._into_file(open(wheel_name, "wb")) + await micropip.install("emfs:" + wheel_name) + import snowballstemmer - stemmer = snowballstemmer.stemmer("english") - assert stemmer.stemWords("go going goes gone".split()) == [ - "go", - "go", - "goe", - "gone", - ] + stemmer = snowballstemmer.stemmer("english") + assert stemmer.stemWords("go going goes gone".split()) == [ + "go", + "go", + "goe", + "gone", + ] - run_test(selenium_standalone_micropip, url, SNOWBALL_WHEEL) + run_test(selenium_standalone_micropip, snowball_wheel.url, snowball_wheel.filename) -def test_logging(selenium_standalone_micropip): - # TODO: make a fixture for this, it's used in a few places - with spawn_web_server(TEST_WHEEL_DIR) as server: - server_hostname, server_port, _ = server - url = f"http://{server_hostname}:{server_port}/" - wheel_url = url + SNOWBALL_WHEEL - name, version, _, _ = parse_wheel_filename(SNOWBALL_WHEEL) +def test_logging(selenium_standalone_micropip, wheel_catalog): + @run_in_pyodide(packages=["micropip"]) + async def run_test(selenium, url, name, version): + import contextlib + import io - @run_in_pyodide(packages=["micropip"]) - async def run_test(selenium, url, name, version): - import contextlib - import io + import micropip - import micropip + with io.StringIO() as buf, contextlib.redirect_stdout(buf): + await micropip.install(url, verbose=True) - with io.StringIO() as buf, contextlib.redirect_stdout(buf): - await micropip.install(url, verbose=True) + captured = buf.getvalue() - captured = buf.getvalue() + assert f"Collecting {name}" in captured + assert f" Downloading {name}" in captured + assert f"Installing collected packages: {name}" in captured + assert f"Successfully installed {name}-{version}" in captured - assert f"Collecting {name}" in captured - assert f" Downloading {name}" in captured - assert f"Installing collected packages: {name}" in captured - assert f"Successfully installed {name}-{version}" in captured + snowball_wheel = wheel_catalog.get("snowballstemmer") + wheel_url = snowball_wheel.url + name, version, _, _ = parse_wheel_filename(snowball_wheel.filename) - run_test(selenium_standalone_micropip, wheel_url, name, version) + run_test(selenium_standalone_micropip, wheel_url, name, version) @pytest.mark.asyncio diff --git a/tests/test_list.py b/tests/test_list.py index 16d45ce..d229b9f 100644 --- a/tests/test_list.py +++ b/tests/test_list.py @@ -1,6 +1,5 @@ import pytest -from conftest import SNOWBALL_WHEEL, TEST_WHEEL_DIR, mock_fetch_cls -from pytest_pyodide import spawn_web_server +from conftest import mock_fetch_cls import micropip @@ -42,22 +41,20 @@ async def test_list_wheel_name_mismatch(mock_fetch: mock_fetch_cls) -> None: assert pkg_list[dummy_pkg_name].source.lower() == dummy_url -def test_list_load_package_from_url(selenium_standalone_micropip): - with spawn_web_server(TEST_WHEEL_DIR) as server: - server_hostname, server_port, _ = server - base_url = f"http://{server_hostname}:{server_port}/" - url = base_url + SNOWBALL_WHEEL - - selenium = selenium_standalone_micropip - selenium.run_js( - f""" - await pyodide.loadPackage({url!r}); - await pyodide.runPythonAsync(` - import micropip - assert "snowballstemmer" in micropip.list() - `); - """ - ) +def test_list_load_package_from_url(selenium_standalone_micropip, wheel_catalog): + snowball_wheel = wheel_catalog.get("snowballstemmer") + url = snowball_wheel.url + + selenium = selenium_standalone_micropip + selenium.run_js( + f""" + await pyodide.loadPackage({url!r}); + await pyodide.runPythonAsync(` + import micropip + assert "snowballstemmer" in micropip.list() + `); + """ + ) def test_list_pyodide_package(selenium_standalone_micropip): diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 3e878e1..326490d 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -1,7 +1,6 @@ import pytest -from conftest import SNOWBALL_WHEEL, TEST_WHEEL_DIR +from conftest import SNOWBALL_WHEEL from packaging.tags import Tag -from pytest_pyodide import spawn_web_server @pytest.mark.parametrize( @@ -65,23 +64,20 @@ def create_transaction(Transaction): @pytest.mark.asyncio -async def test_add_requirement(): +async def test_add_requirement(wheel_catalog): pytest.importorskip("packaging") from micropip.transaction import Transaction - with spawn_web_server(TEST_WHEEL_DIR) as server: - server_hostname, server_port, _ = server - base_url = f"http://{server_hostname}:{server_port}/" - url = base_url + SNOWBALL_WHEEL + snowballstemmer_wheel = wheel_catalog.get("snowballstemmer") - transaction = create_transaction(Transaction) - await transaction.add_requirement(url) + transaction = create_transaction(Transaction) + await transaction.add_requirement(snowballstemmer_wheel.url) wheel = transaction.wheels[0] assert wheel.name == "snowballstemmer" assert str(wheel.version) == "2.0.0" assert wheel.filename == SNOWBALL_WHEEL - assert wheel.url == url + assert wheel.url == snowballstemmer_wheel.url assert wheel.tags == frozenset( {Tag("py2", "none", "any"), Tag("py3", "none", "any")} ) diff --git a/tests/test_uninstall.py b/tests/test_uninstall.py index 452795a..6f8e9ad 100644 --- a/tests/test_uninstall.py +++ b/tests/test_uninstall.py @@ -1,26 +1,14 @@ # isort: skip_file -import pytest -from pytest_pyodide import run_in_pyodide, spawn_web_server -from conftest import SNOWBALL_WHEEL, TEST_WHEEL_DIR -from packaging.utils import parse_wheel_filename, canonicalize_name +from pytest_pyodide import run_in_pyodide +from packaging.utils import parse_wheel_filename -TEST_PACKAGE_NAME = "test_wheel_uninstall" -TEST_PACKAGE_NAME_NORMALIZED = canonicalize_name(TEST_PACKAGE_NAME) +TEST_PACKAGE_NAME = "test-wheel-uninstall" -@pytest.fixture(scope="module") -def test_wheel_url(test_wheel_path): - wheel_file = next(test_wheel_path.glob(f"{TEST_PACKAGE_NAME}-*.whl")).name - - with spawn_web_server(test_wheel_path) as server: - server_hostname, server_port, _ = server - yield f"http://{server_hostname}:{server_port}/{wheel_file}" - - -def test_basic(selenium_standalone_micropip, test_wheel_url): +def test_basic(selenium_standalone_micropip, wheel_catalog): @run_in_pyodide() - async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): + async def run(selenium, pkg_name, import_name, wheel_url): import importlib.metadata import sys @@ -28,18 +16,18 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): await micropip.install(wheel_url) - assert pkg_name_normalized in micropip.list() - assert pkg_name not in sys.modules + assert pkg_name in micropip.list() + assert import_name not in sys.modules - __import__(pkg_name) - assert pkg_name in sys.modules + __import__(import_name) + assert import_name in sys.modules micropip.uninstall(pkg_name) - del sys.modules[pkg_name] + del sys.modules[import_name] # 1. Check that the module is not available with import statement try: - __import__(pkg_name) + __import__(import_name) except ImportError: pass else: @@ -53,27 +41,29 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): # 3. Check that the module is not available with micropip.list() assert pkg_name not in micropip.list() + test_wheel = wheel_catalog.get(TEST_PACKAGE_NAME) + run( selenium_standalone_micropip, - TEST_PACKAGE_NAME, - TEST_PACKAGE_NAME_NORMALIZED, - test_wheel_url, + test_wheel.name, + test_wheel.top_level, + test_wheel.url, ) -def test_files(selenium_standalone_micropip, test_wheel_url): +def test_files(selenium_standalone_micropip, wheel_catalog): """ Check all files are removed after uninstallation. """ @run_in_pyodide() - async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): + async def run(selenium, pkg_name, wheel_url): import importlib.metadata import micropip await micropip.install(wheel_url) - assert pkg_name_normalized in micropip.list() + assert pkg_name in micropip.list() dist = importlib.metadata.distribution(pkg_name) files = dist.files @@ -92,39 +82,40 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): assert not dist._path.is_dir(), f"{dist._path} still exists after removal" + test_wheel = wheel_catalog.get(TEST_PACKAGE_NAME) + run( selenium_standalone_micropip, - TEST_PACKAGE_NAME, - TEST_PACKAGE_NAME_NORMALIZED, - test_wheel_url, + test_wheel.name, + test_wheel.url, ) -def test_install_again(selenium_standalone_micropip, test_wheel_url): +def test_install_again(selenium_standalone_micropip, wheel_catalog): """ Check that uninstalling and installing again works. """ @run_in_pyodide() - async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): + async def run(selenium, pkg_name, import_name, wheel_url): import sys import micropip await micropip.install(wheel_url) - assert pkg_name_normalized in micropip.list() + assert pkg_name in micropip.list() - __import__(pkg_name) + __import__(import_name) micropip.uninstall(pkg_name) - assert pkg_name_normalized not in micropip.list() + assert pkg_name not in micropip.list() - del sys.modules[pkg_name] + del sys.modules[import_name] try: - __import__(pkg_name) + __import__(import_name) except ImportError: pass else: @@ -132,14 +123,16 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): await micropip.install(wheel_url) - assert pkg_name_normalized in micropip.list() - __import__(pkg_name) + assert pkg_name in micropip.list() + __import__(import_name) + + test_wheel = wheel_catalog.get(TEST_PACKAGE_NAME) run( selenium_standalone_micropip, - TEST_PACKAGE_NAME, - TEST_PACKAGE_NAME_NORMALIZED, - test_wheel_url, + test_wheel.name, + test_wheel.top_level, + test_wheel.url, ) @@ -166,13 +159,13 @@ async def run(selenium): run(selenium_standalone_micropip) -def test_warning_file_removed(selenium_standalone_micropip, test_wheel_url): +def test_warning_file_removed(selenium_standalone_micropip, wheel_catalog): """ Test warning when files in a package are removed by user. """ @run_in_pyodide() - async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): + async def run(selenium, pkg_name, wheel_url): from importlib.metadata import distribution import micropip import contextlib @@ -181,9 +174,9 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): with io.StringIO() as buf, contextlib.redirect_stdout(buf): await micropip.install(wheel_url) - assert pkg_name_normalized in micropip.list() + assert pkg_name in micropip.list() - dist = distribution(pkg_name_normalized) + dist = distribution(pkg_name) files = dist.files file1 = files[0] file2 = files[1] @@ -191,7 +184,7 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): file1.locate().unlink() file2.locate().unlink() - micropip.uninstall(pkg_name_normalized) + micropip.uninstall(pkg_name) captured = buf.getvalue() logs = captured.strip().split("\n") @@ -200,21 +193,22 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): assert "does not exist" in logs[-1] assert "does not exist" in logs[-2] + test_wheel = wheel_catalog.get(TEST_PACKAGE_NAME) + run( selenium_standalone_micropip, - TEST_PACKAGE_NAME, - TEST_PACKAGE_NAME_NORMALIZED, - test_wheel_url, + test_wheel.name, + test_wheel.url, ) -def test_warning_remaining_file(selenium_standalone_micropip, test_wheel_url): +def test_warning_remaining_file(selenium_standalone_micropip, wheel_catalog): """ Test warning when there are remaining files after uninstallation. """ @run_in_pyodide() - async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): + async def run(selenium, pkg_name, wheel_url): from importlib.metadata import distribution import micropip import contextlib @@ -222,12 +216,12 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): with io.StringIO() as buf, contextlib.redirect_stdout(buf): await micropip.install(wheel_url) - assert pkg_name_normalized in micropip.list() + assert pkg_name in micropip.list() - pkg_dir = distribution(pkg_name_normalized)._path.parent / "deep" + pkg_dir = distribution(pkg_name)._path.parent / "deep" (pkg_dir / "extra-file.txt").touch() - micropip.uninstall(pkg_name_normalized) + micropip.uninstall(pkg_name) captured = buf.getvalue() logs = captured.strip().split("\n") @@ -235,11 +229,12 @@ async def run(selenium, pkg_name, pkg_name_normalized, wheel_url): assert len(logs) == 1 assert "is not empty after uninstallation" in logs[0] + test_wheel = wheel_catalog.get(TEST_PACKAGE_NAME) + run( selenium_standalone_micropip, - TEST_PACKAGE_NAME, - TEST_PACKAGE_NAME_NORMALIZED, - test_wheel_url, + test_wheel.name, + test_wheel.url, ) @@ -272,27 +267,24 @@ async def run(selenium): run(selenium_standalone_micropip) -def test_logging(selenium_standalone_micropip): - # TODO: make a fixture for this, it's used in a few places - with spawn_web_server(TEST_WHEEL_DIR) as server: - server_hostname, server_port, _ = server - url = f"http://{server_hostname}:{server_port}/" - wheel_url = url + SNOWBALL_WHEEL - name, version, _, _ = parse_wheel_filename(SNOWBALL_WHEEL) +def test_logging(selenium_standalone_micropip, wheel_catalog): + snowballstemmer_wheel = wheel_catalog.get("snowballstemmer") + wheel_url = snowballstemmer_wheel.url + name, version, _, _ = parse_wheel_filename(snowballstemmer_wheel.filename) - @run_in_pyodide(packages=["micropip"]) - async def run_test(selenium, url, name, version): - import micropip - import contextlib - import io + @run_in_pyodide(packages=["micropip"]) + async def run_test(selenium, url, name, version): + import micropip + import contextlib + import io - with io.StringIO() as buf, contextlib.redirect_stdout(buf): - await micropip.install(url) - micropip.uninstall("snowballstemmer", verbose=True) + with io.StringIO() as buf, contextlib.redirect_stdout(buf): + await micropip.install(url) + micropip.uninstall("snowballstemmer", verbose=True) - captured = buf.getvalue() + captured = buf.getvalue() - assert f"Found existing installation: {name} {version}" in captured - assert f"Successfully uninstalled {name}-{version}" in captured + assert f"Found existing installation: {name} {version}" in captured + assert f"Successfully uninstalled {name}-{version}" in captured - run_test(selenium_standalone_micropip, wheel_url, name, version) + run_test(selenium_standalone_micropip, wheel_url, name, version) diff --git a/tests/test_wheelinfo.py b/tests/test_wheelinfo.py index 8e027b5..ddbb91b 100644 --- a/tests/test_wheelinfo.py +++ b/tests/test_wheelinfo.py @@ -1,29 +1,8 @@ import pytest -from conftest import PYTEST_WHEEL, TEST_WHEEL_DIR from micropip.wheelinfo import WheelInfo -@pytest.fixture -def dummy_wheel(): - yield WheelInfo.from_url(f"https://test.com/{PYTEST_WHEEL}") - - -@pytest.fixture -def dummy_wheel_content(): - yield (TEST_WHEEL_DIR / PYTEST_WHEEL).read_bytes() - - -@pytest.fixture -def dummy_wheel_url(httpserver): - httpserver.expect_request(f"/{PYTEST_WHEEL}").respond_with_data( - (TEST_WHEEL_DIR / PYTEST_WHEEL).read_bytes(), - content_type="application/zip", - headers={"Access-Control-Allow-Origin": "*"}, - ) - return httpserver.url_for(f"/{PYTEST_WHEEL}") - - def test_from_url(): url = "https://test.com/dummy_module-0.0.1-py3-none-any.whl" wheel = WheelInfo.from_url(url) @@ -54,16 +33,21 @@ def test_from_package_index(): assert wheel.sha256 == sha256 -def test_extract(dummy_wheel, dummy_wheel_content, tmp_path): - dummy_wheel._data = dummy_wheel_content - dummy_wheel._extract(tmp_path) +def test_extract(wheel_catalog, tmp_path): + pytest_wheel = wheel_catalog.get("pytest") + dummy_wheel = WheelInfo.from_url(pytest_wheel.url) + dummy_wheel._data = pytest_wheel.content + dummy_wheel._extract(tmp_path) assert dummy_wheel._dist_info is not None assert dummy_wheel._dist_info.is_dir() -def test_set_installer(dummy_wheel, dummy_wheel_content, tmp_path): - dummy_wheel._data = dummy_wheel_content +def test_set_installer(wheel_catalog, tmp_path): + pytest_wheel = wheel_catalog.get("pytest") + dummy_wheel = WheelInfo.from_url(pytest_wheel.url) + dummy_wheel._data = pytest_wheel.content + dummy_wheel._extract(tmp_path) dummy_wheel._set_installer() @@ -79,8 +63,9 @@ def test_install(): @pytest.mark.asyncio -async def test_download(dummy_wheel_url): - wheel = WheelInfo.from_url(dummy_wheel_url) +async def test_download(wheel_catalog): + pytest_wheel = wheel_catalog.get("pytest") + wheel = WheelInfo.from_url(pytest_wheel.url) assert wheel._metadata is None @@ -90,8 +75,9 @@ async def test_download(dummy_wheel_url): @pytest.mark.asyncio -async def test_requires(dummy_wheel_url, tmp_path): - wheel = WheelInfo.from_url(dummy_wheel_url) +async def test_requires(wheel_catalog, tmp_path): + pytest_wheel = wheel_catalog.get("pytest") + wheel = WheelInfo.from_url(pytest_wheel.url) await wheel.download({}) wheel._extract(tmp_path)