You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible/test/integration/targets/canonical-pep517-self-packa.../runme_test.py

381 lines
14 KiB
Python

"""Smoke tests for the in-tree PEP 517 backend."""
from __future__ import annotations
from filecmp import dircmp
from os import chdir, environ, PathLike
from pathlib import Path
from shutil import rmtree
from subprocess import check_call, check_output, PIPE
from sys import executable as current_interpreter, version_info
from tarfile import TarFile
import typing as t
try:
from contextlib import chdir as _chdir_cm
except ImportError:
from contextlib import contextmanager as _contextmanager
@_contextmanager
def _chdir_cm(path: PathLike) -> t.Iterator[None]:
original_wd = Path.cwd()
chdir(path)
try:
yield
finally:
chdir(original_wd)
import pytest
DIST_NAME = 'ansible_core'
DIST_FILENAME_BASE = 'ansible-core'
OUTPUT_DIR = Path(environ['OUTPUT_DIR']).resolve().absolute()
SRC_ROOT_DIR = OUTPUT_DIR.parents[3]
GENERATED_MANPAGES_SUBDIR = SRC_ROOT_DIR / 'docs' / 'man' / 'man1'
LOWEST_SUPPORTED_BUILD_DEPS_FILE = (
Path(__file__).parent / 'minimum-build-constraints.txt'
).resolve().absolute()
MODERNISH_BUILD_DEPS_FILE = (
Path(__file__).parent / 'modernish-build-constraints.txt'
).resolve().absolute()
RELEASE_MODULE = SRC_ROOT_DIR / 'lib' / 'ansible' / 'release.py'
VERSION_LINE_PREFIX = "__version__ = '"
PKG_DIST_VERSION = next(
line[len(VERSION_LINE_PREFIX):-1]
for line in RELEASE_MODULE.read_text().splitlines()
if line.startswith(VERSION_LINE_PREFIX)
)
EXPECTED_SDIST_NAME_BASE = f'{DIST_FILENAME_BASE}-{PKG_DIST_VERSION}'
EXPECTED_SDIST_NAME = f'{EXPECTED_SDIST_NAME_BASE}.tar.gz'
EXPECTED_WHEEL_NAME = f'{DIST_NAME}-{PKG_DIST_VERSION}-py3-none-any.whl'
IS_PYTHON310_PLUS = version_info[:2] >= (3, 10)
def wipe_generated_manpages() -> None:
"""Ensure man1 pages aren't present in the source checkout."""
# Cleaning up the gitignored manpages...
if not GENERATED_MANPAGES_SUBDIR.exists():
return
rmtree(GENERATED_MANPAGES_SUBDIR)
# Removed the generated manpages...
def contains_man1_pages(sdist_tarball: Path) -> Path:
"""Check if the man1 pages are present in given tarball."""
with sdist_tarball.open(mode='rb') as tarball_fd:
with TarFile.gzopen(fileobj=tarball_fd, name=None) as tarball:
try:
tarball.getmember(
name=f'{EXPECTED_SDIST_NAME_BASE}/docs/man/man1',
)
except KeyError:
return False
return True
def unpack_sdist(sdist_tarball: Path, target_directory: Path) -> Path:
"""Unarchive given tarball.
:returns: Path of the package source checkout.
"""
with sdist_tarball.open(mode='rb') as tarball_fd:
with TarFile.gzopen(fileobj=tarball_fd, name=None) as tarball:
tarball.extractall(path=target_directory)
return target_directory / EXPECTED_SDIST_NAME_BASE
def assert_dirs_equal(*dir_paths: t.List[Path]) -> None:
dir_comparison = dircmp(*dir_paths)
assert not dir_comparison.left_only
assert not dir_comparison.right_only
assert not dir_comparison.diff_files
assert not dir_comparison.funny_files
def normalize_unpacked_rebuilt_sdist(sdist_path: Path) -> None:
top_pkg_info_path = sdist_path / 'PKG-INFO'
nested_pkg_info_path = (
sdist_path / 'lib' / f'{DIST_NAME}.egg-info' / 'PKG-INFO'
)
entry_points_path = nested_pkg_info_path.parent / 'entry_points.txt'
# setuptools v39 write out two trailing empty lines and an unknown platform
# while the recent don't
top_pkg_info_path.write_text(
top_pkg_info_path.read_text().replace(
'Classifier: Development Status :: 5',
'Platform: UNKNOWN\nClassifier: Development Status :: 5',
) + '\n\n'
)
nested_pkg_info_path.write_text(
nested_pkg_info_path.read_text().replace(
'Classifier: Development Status :: 5',
'Platform: UNKNOWN\nClassifier: Development Status :: 5',
) + '\n\n'
)
# setuptools v39 write out one trailing empty line while the recent don't
entry_points_path.write_text(entry_points_path.read_text() + '\n')
@pytest.fixture
def venv_python_exe(tmp_path: Path) -> t.Iterator[Path]:
venv_path = tmp_path / 'pytest-managed-venv'
mkvenv_cmd = (
current_interpreter, '-m', 'venv', str(venv_path),
)
check_call(mkvenv_cmd, env={}, stderr=PIPE, stdout=PIPE)
yield venv_path / 'bin' / 'python'
rmtree(venv_path)
def run_with_venv_python(
python_exe: Path, *cli_args: t.Iterable[str],
env_vars: t.Dict[str, str] = None,
) -> str:
if env_vars is None:
env_vars = {}
full_cmd = str(python_exe), *cli_args
return check_output(full_cmd, env=env_vars, stderr=PIPE)
def build_dists(
python_exe: Path, *cli_args: t.Iterable[str],
env_vars: t.Dict[str, str],
) -> str:
return run_with_venv_python(
python_exe, '-m', 'build',
*cli_args, env_vars=env_vars,
)
def pip_install(
python_exe: Path, *cli_args: t.Iterable[str],
env_vars: t.Dict[str, str] = None,
) -> str:
return run_with_venv_python(
python_exe, '-m', 'pip', 'install',
*cli_args, env_vars=env_vars,
)
def test_installing_sdist_build_with_modern_deps_to_old_env(
venv_python_exe: Path, tmp_path: Path,
) -> None:
pip_install(venv_python_exe, 'build ~= 0.10.0')
tmp_dir_sdist_w_modern_tools = tmp_path / 'sdist-w-modern-tools'
build_dists(
venv_python_exe, '--sdist',
'--config-setting=--build-manpages',
f'--outdir={tmp_dir_sdist_w_modern_tools!s}',
str(SRC_ROOT_DIR),
env_vars={
'PIP_CONSTRAINT': str(MODERNISH_BUILD_DEPS_FILE),
},
)
tmp_path_sdist_w_modern_tools = (
tmp_dir_sdist_w_modern_tools / EXPECTED_SDIST_NAME
)
# Downgrading pip, because v20+ supports in-tree build backends
pip_install(venv_python_exe, 'pip ~= 19.3.1')
# Smoke test — installing an sdist with pip that does not support
# in-tree build backends.
pip_install(
venv_python_exe, str(tmp_path_sdist_w_modern_tools), '--no-deps',
)
# Downgrading pip, because versions that support PEP 517 don't allow
# disabling it with `--no-use-pep517` when `build-backend` is set in
# the `[build-system]` section of `pyproject.toml`, considering this
# an explicit opt-in.
if not IS_PYTHON310_PLUS:
pip_install(venv_python_exe, 'pip == 18.0')
# Smoke test — installing an sdist with pip that does not support invoking
# PEP 517 interface at all.
# In this scenario, pip will run `setup.py install` since `wheel` is not in
# the environment.
if IS_PYTHON310_PLUS:
tmp_dir_unpacked_sdist_root = tmp_path / 'unpacked-sdist'
tmp_dir_unpacked_sdist_path = tmp_dir_unpacked_sdist_root / EXPECTED_SDIST_NAME_BASE
with TarFile.gzopen(tmp_path_sdist_w_modern_tools) as sdist_fd:
sdist_fd.extractall(path=tmp_dir_unpacked_sdist_root)
with _chdir_cm(tmp_dir_unpacked_sdist_path):
run_with_venv_python(
venv_python_exe, 'setup.py', 'sdist',
env_vars={'PATH': environ['PATH']},
)
run_with_venv_python(
venv_python_exe, 'setup.py', 'install',
env_vars={'PATH': environ['PATH']},
)
else:
pip_install(
venv_python_exe, str(tmp_path_sdist_w_modern_tools), '--no-deps',
)
# Smoke test — installing an sdist with pip that does not support invoking
# PEP 517 interface at all.
# With `wheel` present, pip will run `setup.py bdist_wheel` and then,
# unpack the result.
pip_install(venv_python_exe, 'wheel')
if IS_PYTHON310_PLUS:
with _chdir_cm(tmp_dir_unpacked_sdist_path):
run_with_venv_python(
venv_python_exe, 'setup.py', 'bdist_wheel',
env_vars={'PATH': environ['PATH']},
)
else:
pip_install(
venv_python_exe, str(tmp_path_sdist_w_modern_tools), '--no-deps',
)
def test_dist_rebuilds_with_manpages_premutations(
venv_python_exe: Path, tmp_path: Path,
) -> None:
"""Test a series of sdist rebuilds under different conditions.
This check builds sdists right from the Git checkout with and without
the manpages. It also does this using different versions of the setuptools
PEP 517 build backend being pinned. Finally, it builds a wheel out of one
of the rebuilt sdists.
As intermediate assertions, this test makes simple smoke tests along
the way.
"""
pip_install(venv_python_exe, 'build ~= 0.10.0')
# Test building an sdist without manpages from the Git checkout
tmp_dir_sdist_without_manpages = tmp_path / 'sdist-without-manpages'
wipe_generated_manpages()
build_dists(
venv_python_exe, '--sdist',
f'--outdir={tmp_dir_sdist_without_manpages!s}',
str(SRC_ROOT_DIR),
env_vars={
'PIP_CONSTRAINT': str(MODERNISH_BUILD_DEPS_FILE),
},
)
tmp_path_sdist_without_manpages = (
tmp_dir_sdist_without_manpages / EXPECTED_SDIST_NAME
)
assert tmp_path_sdist_without_manpages.exists()
assert not contains_man1_pages(tmp_path_sdist_without_manpages)
sdist_without_manpages_path = unpack_sdist(
tmp_path_sdist_without_manpages,
tmp_dir_sdist_without_manpages / 'src',
)
# Test building an sdist with manpages from the Git checkout
# and lowest supported build deps
wipe_generated_manpages()
tmp_dir_sdist_with_manpages = tmp_path / 'sdist-with-manpages'
build_dists(
venv_python_exe, '--sdist',
'--config-setting=--build-manpages',
f'--outdir={tmp_dir_sdist_with_manpages!s}',
str(SRC_ROOT_DIR),
env_vars={
'PIP_CONSTRAINT': str(LOWEST_SUPPORTED_BUILD_DEPS_FILE),
},
)
tmp_path_sdist_with_manpages = (
tmp_dir_sdist_with_manpages / EXPECTED_SDIST_NAME
)
assert tmp_path_sdist_with_manpages.exists()
assert contains_man1_pages(tmp_path_sdist_with_manpages)
sdist_with_manpages_path = unpack_sdist(
tmp_path_sdist_with_manpages,
tmp_dir_sdist_with_manpages / 'src',
)
# Test re-building an sdist with manpages from the
# sdist contents that does not include the manpages
tmp_dir_rebuilt_sdist = tmp_path / 'rebuilt-sdist'
build_dists(
venv_python_exe, '--sdist',
'--config-setting=--build-manpages',
f'--outdir={tmp_dir_rebuilt_sdist!s}',
str(sdist_without_manpages_path),
env_vars={
'PIP_CONSTRAINT': str(MODERNISH_BUILD_DEPS_FILE),
},
)
tmp_path_rebuilt_sdist = tmp_dir_rebuilt_sdist / EXPECTED_SDIST_NAME
# Checking that the expected sdist got created
# from the previous unpacked sdist...
assert tmp_path_rebuilt_sdist.exists()
# NOTE: The following assertion is disabled due to the fact that, when
# NOTE: building an sdist from the original source checkout, the build
# NOTE: backend replaces itself with pure setuptools in the resulting
# NOTE: sdist, and the following rebuilds from that sdist are no longer
# NOTE: able to process the custom config settings that are implemented in
# NOTE: the in-tree build backend. It is expected that said
# NOTE: `pyproject.toml` mutation change will be reverted once all of the
# NOTE: supported `ansible-core` versions ship wheels, meaning that the
# NOTE: end-users won't be building the distribution from sdist on install.
# NOTE: Another case, when it can be reverted is declaring pip below v20
# NOTE: unsupported — it is the first version to support in-tree build
# NOTE: backends natively.
# assert contains_man1_pages(tmp_path_rebuilt_sdist) # FIXME: See #80255
rebuilt_sdist_path = unpack_sdist(
tmp_path_rebuilt_sdist,
tmp_dir_rebuilt_sdist / 'src',
)
assert rebuilt_sdist_path.exists()
assert rebuilt_sdist_path.is_dir()
normalize_unpacked_rebuilt_sdist(rebuilt_sdist_path)
assert_dirs_equal(rebuilt_sdist_path, sdist_with_manpages_path)
# Test building a wheel from the rebuilt sdist with manpages contents
# and lowest supported build deps
tmp_dir_rebuilt_wheel = tmp_path / 'rebuilt-wheel'
build_dists(
venv_python_exe, '--wheel',
f'--outdir={tmp_dir_rebuilt_wheel!s}',
str(sdist_with_manpages_path),
env_vars={
'PIP_CONSTRAINT': str(LOWEST_SUPPORTED_BUILD_DEPS_FILE),
},
)
tmp_path_rebuilt_wheel = tmp_dir_rebuilt_wheel / EXPECTED_WHEEL_NAME
# Checking that the expected wheel got created...
assert tmp_path_rebuilt_wheel.exists()
def test_pep660_editable_install_smoke(venv_python_exe: Path) -> None:
"""Smoke-test PEP 660 editable install.
This verifies that the in-tree build backend wrapper
does not break any required interfaces.
"""
pip_install(venv_python_exe, '-e', str(SRC_ROOT_DIR))
pip_show_cmd = (
str(venv_python_exe), '-m',
'pip', 'show', DIST_FILENAME_BASE,
)
installed_ansible_meta = check_output(
pip_show_cmd,
env={}, stderr=PIPE, text=True,
).splitlines()
assert f'Name: {DIST_FILENAME_BASE}' in installed_ansible_meta
assert f'Version: {PKG_DIST_VERSION}' in installed_ansible_meta
pip_runtime_version_cmd = (
str(venv_python_exe), '-c',
'from ansible import __version__; print(__version__)',
)
runtime_ansible_version = check_output(
pip_runtime_version_cmd,
env={}, stderr=PIPE, text=True,
).strip()
assert runtime_ansible_version == PKG_DIST_VERSION