"""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