mirror of https://github.com/pyodide/pyodide.git
549 lines
15 KiB
Python
549 lines
15 KiB
Python
# flake8: noqa
|
|
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pytest_pyodide import spawn_web_server
|
|
import zipfile
|
|
import typer
|
|
from typer.testing import CliRunner
|
|
from typing import Any
|
|
import pyodide_build
|
|
from pyodide_build import common, build_env, cli
|
|
from pyodide_build.cli import (
|
|
build,
|
|
build_recipes,
|
|
config,
|
|
create_zipfile,
|
|
skeleton,
|
|
xbuildenv,
|
|
py_compile,
|
|
)
|
|
|
|
from .fixture import temp_python_lib, temp_python_lib2, temp_xbuildenv
|
|
|
|
only_node = pytest.mark.xfail_browsers(
|
|
chrome="node only", firefox="node only", safari="node only"
|
|
)
|
|
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
def test_skeleton_pypi(tmp_path):
|
|
test_pkg = "pytest-pyodide"
|
|
old_version = "0.21.0"
|
|
new_version = "0.22.0"
|
|
|
|
result = runner.invoke(
|
|
skeleton.app,
|
|
[
|
|
"pypi",
|
|
test_pkg,
|
|
"--recipe-dir",
|
|
str(tmp_path),
|
|
"--version",
|
|
old_version,
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "pytest-pyodide/meta.yaml" in result.stdout
|
|
|
|
result = runner.invoke(
|
|
skeleton.app,
|
|
[
|
|
"pypi",
|
|
test_pkg,
|
|
"--recipe-dir",
|
|
str(tmp_path),
|
|
"--version",
|
|
new_version,
|
|
"--update",
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert f"Updated {test_pkg} from {old_version} to {new_version}" in result.stdout
|
|
|
|
result = runner.invoke(
|
|
skeleton.app, ["pypi", test_pkg, "--recipe-dir", str(tmp_path)]
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "already exists" in str(result.exception)
|
|
|
|
|
|
def test_build_recipe(selenium, tmp_path):
|
|
# TODO: Run this test without building Pyodide
|
|
|
|
output_dir = tmp_path / "dist"
|
|
recipe_dir = Path(__file__).parent / "_test_recipes"
|
|
|
|
pkgs = {
|
|
"pkg_test_tag_always": {},
|
|
"pkg_test_graph1": {"pkg_test_graph2"},
|
|
"pkg_test_graph3": {},
|
|
}
|
|
|
|
pkgs_to_build = pkgs.keys() | {p for v in pkgs.values() for p in v}
|
|
|
|
for build_dir in recipe_dir.rglob("build"):
|
|
shutil.rmtree(build_dir)
|
|
|
|
app = typer.Typer()
|
|
app.command()(build_recipes.recipe)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
*pkgs.keys(),
|
|
"--recipe-dir",
|
|
str(recipe_dir),
|
|
"--install",
|
|
"--install-dir",
|
|
str(output_dir),
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
|
|
for pkg in pkgs_to_build:
|
|
assert f"built {pkg} in" in result.stdout
|
|
|
|
built_wheels = set(output_dir.glob("*.whl"))
|
|
assert len(built_wheels) == len(pkgs_to_build)
|
|
|
|
|
|
def test_build_recipe_no_deps(selenium, tmp_path):
|
|
# TODO: Run this test without building Pyodide
|
|
|
|
recipe_dir = Path(__file__).parent / "_test_recipes"
|
|
|
|
for build_dir in recipe_dir.rglob("build"):
|
|
shutil.rmtree(build_dir)
|
|
|
|
app = typer.Typer()
|
|
app.command()(build_recipes.recipe)
|
|
|
|
pkgs_to_build = ["pkg_test_graph1", "pkg_test_graph3"]
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
*pkgs_to_build,
|
|
"--recipe-dir",
|
|
str(recipe_dir),
|
|
"--no-deps",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
|
|
for pkg in pkgs_to_build:
|
|
assert f"Succeeded building package {pkg}" in result.stdout
|
|
|
|
for pkg in pkgs_to_build:
|
|
dist_dir = recipe_dir / pkg / "dist"
|
|
assert len(list(dist_dir.glob("*.whl"))) == 1
|
|
|
|
|
|
def test_build_recipe_no_deps_force_rebuild(selenium, tmp_path):
|
|
# TODO: Run this test without building Pyodide
|
|
|
|
recipe_dir = Path(__file__).parent / "_test_recipes"
|
|
|
|
for build_dir in recipe_dir.rglob("build"):
|
|
shutil.rmtree(build_dir)
|
|
|
|
app = typer.Typer()
|
|
app.command()(build_recipes.recipe)
|
|
|
|
pkg = "pkg_test_graph1"
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
pkg,
|
|
"--recipe-dir",
|
|
str(recipe_dir),
|
|
"--no-deps",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
pkg,
|
|
"--recipe-dir",
|
|
str(recipe_dir),
|
|
"--no-deps",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Creating virtualenv isolated environment" not in result.stdout
|
|
assert f"Succeeded building package {pkg}" in result.stdout
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
pkg,
|
|
"--recipe-dir",
|
|
str(recipe_dir),
|
|
"--no-deps",
|
|
"--force-rebuild",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Creating virtualenv isolated environment" in result.stdout
|
|
assert f"Succeeded building package {pkg}" in result.stdout
|
|
|
|
|
|
def test_build_recipe_no_deps_continue(selenium, tmp_path):
|
|
# TODO: Run this test without building Pyodide
|
|
|
|
recipe_dir = Path(__file__).parent / "_test_recipes"
|
|
|
|
for build_dir in recipe_dir.rglob("build"):
|
|
shutil.rmtree(build_dir)
|
|
|
|
app = typer.Typer()
|
|
app.command()(build_recipes.recipe)
|
|
|
|
pkg = "pkg_test_graph1"
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
pkg,
|
|
"--recipe-dir",
|
|
str(recipe_dir),
|
|
"--no-deps",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert f"Succeeded building package {pkg}" in result.stdout
|
|
|
|
for wheels in (recipe_dir / pkg / "build").rglob("*.whl"):
|
|
wheels.unlink()
|
|
|
|
pyproject_toml = next((recipe_dir / pkg / "build").rglob("pyproject.toml"))
|
|
|
|
# Modify some metadata and check it is applied when rebuilt with --continue flag
|
|
version = "99.99.99"
|
|
with open(pyproject_toml, encoding="utf-8") as f:
|
|
pyproject_data = f.read()
|
|
|
|
pyproject_data = pyproject_data.replace(
|
|
'version = "1.0.0"', f'version = "{version}"'
|
|
)
|
|
|
|
with open(pyproject_toml, "w", encoding="utf-8") as f:
|
|
f.write(pyproject_data)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
pkg,
|
|
"--recipe-dir",
|
|
str(recipe_dir),
|
|
"--no-deps",
|
|
"--continue",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert f"Succeeded building package {pkg}" in result.stdout
|
|
assert f"{pkg}-{version}-py3-none-any.whl" in result.stdout
|
|
|
|
|
|
def test_config_list():
|
|
result = runner.invoke(
|
|
config.app,
|
|
[
|
|
"list",
|
|
],
|
|
)
|
|
|
|
envs = result.stdout.splitlines()
|
|
keys = [env.split("=")[0] for env in envs]
|
|
|
|
for cfg_name in config.PYODIDE_CONFIGS.keys():
|
|
assert cfg_name in keys
|
|
|
|
|
|
@pytest.mark.parametrize("cfg_name,env_var", config.PYODIDE_CONFIGS.items())
|
|
def test_config_get(cfg_name, env_var):
|
|
result = runner.invoke(
|
|
config.app,
|
|
[
|
|
"get",
|
|
cfg_name,
|
|
],
|
|
)
|
|
|
|
assert result.stdout.strip() == build_env.get_build_flag(env_var)
|
|
|
|
|
|
def test_create_zipfile(temp_python_lib, temp_python_lib2, tmp_path):
|
|
from zipfile import ZipFile
|
|
|
|
output = tmp_path / "python.zip"
|
|
|
|
app = typer.Typer()
|
|
app.command()(create_zipfile.main)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
str(temp_python_lib),
|
|
str(temp_python_lib2),
|
|
"--output",
|
|
str(output),
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Zip file created" in result.stdout
|
|
assert output.exists()
|
|
|
|
with ZipFile(output) as zf:
|
|
assert "module1.py" in zf.namelist()
|
|
assert "module2.py" in zf.namelist()
|
|
assert "module3.py" in zf.namelist()
|
|
assert "module4.py" in zf.namelist()
|
|
|
|
|
|
def test_create_zipfile_compile(temp_python_lib, temp_python_lib2, tmp_path):
|
|
from zipfile import ZipFile
|
|
|
|
output = tmp_path / "python.zip"
|
|
|
|
app = typer.Typer()
|
|
app.command()(create_zipfile.main)
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
str(temp_python_lib),
|
|
str(temp_python_lib2),
|
|
"--output",
|
|
str(output),
|
|
"--pycompile",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Zip file created" in result.stdout
|
|
assert output.exists()
|
|
|
|
with ZipFile(output) as zf:
|
|
assert "module1.pyc" in zf.namelist()
|
|
assert "module2.pyc" in zf.namelist()
|
|
assert "module3.pyc" in zf.namelist()
|
|
assert "module4.pyc" in zf.namelist()
|
|
|
|
|
|
def test_xbuildenv_create(selenium, tmp_path):
|
|
# selenium fixture is added to ensure that Pyodide is built... it's a hack
|
|
from conftest import package_is_built
|
|
|
|
envpath = Path(tmp_path) / ".xbuildenv"
|
|
result = runner.invoke(
|
|
xbuildenv.app,
|
|
[
|
|
"create",
|
|
str(envpath),
|
|
"--skip-missing-files",
|
|
],
|
|
)
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "xbuildenv created at" in result.stdout
|
|
assert (envpath / "xbuildenv").exists()
|
|
assert (envpath / "xbuildenv" / "pyodide-root").is_dir()
|
|
assert (envpath / "xbuildenv" / "site-packages-extras").is_dir()
|
|
assert (envpath / "xbuildenv" / "requirements.txt").exists()
|
|
|
|
if not package_is_built("scipy"):
|
|
# creating xbuildenv without building scipy will raise error
|
|
result = runner.invoke(
|
|
xbuildenv.app,
|
|
[
|
|
"create",
|
|
str(tmp_path / ".xbuildenv"),
|
|
],
|
|
)
|
|
assert result.exit_code != 0, result.stdout
|
|
assert isinstance(result.exception, FileNotFoundError), result.exception
|
|
|
|
|
|
def test_xbuildenv_install(tmp_path, temp_xbuildenv):
|
|
envpath = Path(tmp_path) / ".xbuildenv"
|
|
|
|
xbuildenv_url_base, xbuildenv_filename = temp_xbuildenv
|
|
|
|
with spawn_web_server(xbuildenv_url_base) as (hostname, port, _):
|
|
xbuildenv_url = f"http://{hostname}:{port}/{xbuildenv_filename}"
|
|
result = runner.invoke(
|
|
xbuildenv.app,
|
|
[
|
|
"install",
|
|
"--path",
|
|
str(envpath),
|
|
"--download",
|
|
"--url",
|
|
xbuildenv_url,
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Downloading xbuild environment" in result.stdout, result.stdout
|
|
assert "Installing xbuild environment" in result.stdout, result.stdout
|
|
assert (envpath / "xbuildenv" / "pyodide-root").is_dir()
|
|
assert (envpath / "xbuildenv" / "site-packages-extras").is_dir()
|
|
assert (envpath / "xbuildenv" / "requirements.txt").exists()
|
|
|
|
|
|
@pytest.mark.parametrize("target", ["dir", "file"])
|
|
@pytest.mark.parametrize("compression_level", [0, 6])
|
|
def test_py_compile(tmp_path, target, compression_level):
|
|
wheel_path = tmp_path / "python.zip"
|
|
with zipfile.ZipFile(wheel_path, "w", compresslevel=3) as zf:
|
|
zf.writestr("a1.py", "def f():\n pass")
|
|
|
|
if target == "dir":
|
|
target_path = tmp_path
|
|
elif target == "file":
|
|
target_path = wheel_path
|
|
|
|
py_compile.main(
|
|
path=target_path, silent=False, keep=False, compression_level=compression_level
|
|
)
|
|
with zipfile.ZipFile(tmp_path / "python.zip", "r") as fh:
|
|
if compression_level > 0:
|
|
assert fh.filelist[0].compress_type == zipfile.ZIP_DEFLATED
|
|
else:
|
|
assert fh.filelist[0].compress_type == zipfile.ZIP_STORED
|
|
|
|
|
|
def test_build1(selenium, tmp_path, monkeypatch):
|
|
from pyodide_build import pypabuild
|
|
|
|
def mocked_build(srcdir: Path, outdir: Path, env: Any, backend_flags: Any) -> str:
|
|
results["srcdir"] = srcdir
|
|
results["outdir"] = outdir
|
|
results["backend_flags"] = backend_flags
|
|
return str(outdir / "a.whl")
|
|
|
|
from contextlib import nullcontext
|
|
|
|
monkeypatch.setattr(common, "modify_wheel", lambda whl: nullcontext())
|
|
monkeypatch.setattr(build_env, "check_emscripten_version", lambda: None)
|
|
monkeypatch.setattr(build_env, "replace_so_abi_tags", lambda whl: None)
|
|
|
|
monkeypatch.setattr(pypabuild, "build", mocked_build)
|
|
|
|
results: dict[str, Any] = {}
|
|
srcdir = tmp_path / "in"
|
|
outdir = tmp_path / "out"
|
|
srcdir.mkdir()
|
|
app = typer.Typer()
|
|
app.command(**build.main.typer_kwargs)(build.main) # type:ignore[attr-defined]
|
|
result = runner.invoke(app, [str(srcdir), "--outdir", str(outdir), "x", "y", "z"])
|
|
|
|
assert result.exit_code == 0
|
|
assert results["srcdir"] == srcdir
|
|
assert results["outdir"] == outdir
|
|
assert results["backend_flags"] == "x y z"
|
|
|
|
|
|
def test_build2_replace_so_abi_tags(selenium, tmp_path, monkeypatch):
|
|
"""
|
|
We intentionally include an "so" (actually an empty file) with Linux slug in
|
|
the name into the wheel generated from the package in
|
|
replace_so_abi_tags_test_package. Test that `pyodide build` renames it to
|
|
have the Emscripten slug. In order to ensure that this works on non-linux
|
|
machines too, we monkey patch config vars to look like a linux machine.
|
|
"""
|
|
import sysconfig
|
|
|
|
config_vars = sysconfig.get_config_vars()
|
|
config_vars["EXT_SUFFIX"] = ".cpython-311-x86_64-linux-gnu.so"
|
|
config_vars["SOABI"] = "cpython-311-x86_64-linux-gnu"
|
|
|
|
def my_get_config_vars(*args):
|
|
return config_vars
|
|
|
|
monkeypatch.setattr(sysconfig, "get_config_vars", my_get_config_vars)
|
|
|
|
srcdir = Path(__file__).parent / "replace_so_abi_tags_test_package"
|
|
outdir = tmp_path / "out"
|
|
app = typer.Typer()
|
|
app.command(**build.main.typer_kwargs)(build.main) # type:ignore[attr-defined]
|
|
result = runner.invoke(app, [str(srcdir), "--outdir", str(outdir)])
|
|
wheel_file = next(outdir.glob("*.whl"))
|
|
print(zipfile.ZipFile(wheel_file).namelist())
|
|
so_file = next(
|
|
x for x in zipfile.ZipFile(wheel_file).namelist() if x.endswith(".so")
|
|
)
|
|
assert so_file.endswith(".cpython-311-wasm32-emscripten.so")
|
|
|
|
|
|
def test_build_exports(monkeypatch):
|
|
def download_url_shim(url, tmppath):
|
|
(tmppath / "build").mkdir()
|
|
return "blah"
|
|
|
|
def unpack_archive_shim(*args):
|
|
pass
|
|
|
|
exports_ = None
|
|
|
|
def run_shim(builddir, output_directory, exports, backend_flags):
|
|
nonlocal exports_
|
|
exports_ = exports
|
|
|
|
monkeypatch.setattr(cli.build, "check_emscripten_version", lambda: None)
|
|
monkeypatch.setattr(cli.build, "download_url", download_url_shim)
|
|
monkeypatch.setattr(shutil, "unpack_archive", unpack_archive_shim)
|
|
monkeypatch.setattr(pyodide_build.out_of_tree.build, "run", run_shim)
|
|
|
|
app = typer.Typer()
|
|
app.command()(build.main)
|
|
|
|
def run(*args):
|
|
nonlocal exports_
|
|
exports_ = None
|
|
result = runner.invoke(
|
|
app,
|
|
[".", *args],
|
|
)
|
|
print("output", result.output)
|
|
return result
|
|
|
|
run()
|
|
assert exports_ == "requested"
|
|
r = run("--exports", "pyinit")
|
|
assert r.exit_code == 0
|
|
assert exports_ == "pyinit"
|
|
r = run("--exports", "a,")
|
|
assert r.exit_code == 0
|
|
assert exports_ == ["a"]
|
|
monkeypatch.setenv("PYODIDE_BUILD_EXPORTS", "whole_archive")
|
|
r = run()
|
|
assert r.exit_code == 0
|
|
assert exports_ == "whole_archive"
|
|
r = run("--exports", "a,")
|
|
assert r.exit_code == 0
|
|
assert exports_ == ["a"]
|
|
r = run("--exports", "a,b,c")
|
|
assert r.exit_code == 0
|
|
assert exports_ == ["a", "b", "c"]
|
|
r = run("--exports", "x")
|
|
assert r.exit_code == 1
|
|
assert (
|
|
r.output.strip().replace("\n", " ").replace(" ", " ")
|
|
== 'Expected exports to be one of "pyinit", "requested", "whole_archive", or a comma separated list of symbols to export. Got "x".'
|
|
)
|