summaryrefslogtreecommitdiffstats
path: root/test/sanity/code-smell/package-data.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/sanity/code-smell/package-data.py')
-rw-r--r--test/sanity/code-smell/package-data.py185
1 files changed, 185 insertions, 0 deletions
diff --git a/test/sanity/code-smell/package-data.py b/test/sanity/code-smell/package-data.py
new file mode 100644
index 0000000..7a81b75
--- /dev/null
+++ b/test/sanity/code-smell/package-data.py
@@ -0,0 +1,185 @@
+"""Verify the contents of the built sdist and wheel."""
+from __future__ import annotations
+
+import contextlib
+import fnmatch
+import os
+import pathlib
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+import typing as t
+import zipfile
+
+from ansible.release import __version__
+
+
+def collect_sdist_files(complete_file_list: list[str]) -> list[str]:
+ """Return a list of files which should be present in the sdist."""
+ ignore_patterns = (
+ '.azure-pipelines/*',
+ '.cherry_picker.toml',
+ '.git*',
+ '.mailmap',
+ 'changelogs/README.md',
+ 'changelogs/config.yaml',
+ 'changelogs/fragments/*',
+ 'hacking/*',
+ )
+
+ sdist_files = [path for path in complete_file_list if not any(fnmatch.fnmatch(path, ignore) for ignore in ignore_patterns)]
+
+ egg_info = (
+ 'PKG-INFO',
+ 'SOURCES.txt',
+ 'dependency_links.txt',
+ 'entry_points.txt',
+ 'not-zip-safe',
+ 'requires.txt',
+ 'top_level.txt',
+ )
+
+ sdist_files.append('PKG-INFO')
+ sdist_files.extend(f'lib/ansible_core.egg-info/{name}' for name in egg_info)
+
+ return sdist_files
+
+
+def collect_wheel_files(complete_file_list: list[str]) -> list[str]:
+ """Return a list of files which should be present in the wheel."""
+ wheel_files = []
+
+ for path in complete_file_list:
+ if path.startswith('lib/ansible/'):
+ prefix = 'lib'
+ elif path.startswith('test/lib/ansible_test/'):
+ prefix = 'test/lib'
+ else:
+ continue
+
+ wheel_files.append(os.path.relpath(path, prefix))
+
+ dist_info = (
+ 'COPYING',
+ 'METADATA',
+ 'RECORD',
+ 'WHEEL',
+ 'entry_points.txt',
+ 'top_level.txt',
+ )
+
+ wheel_files.append(f'ansible_core-{__version__}.data/scripts/ansible-test')
+ wheel_files.extend(f'ansible_core-{__version__}.dist-info/{name}' for name in dist_info)
+
+ return wheel_files
+
+
+@contextlib.contextmanager
+def clean_repository(complete_file_list: list[str]) -> t.Generator[str, None, None]:
+ """Copy the files to a temporary directory and yield the path."""
+ directories = sorted(set(os.path.dirname(path) for path in complete_file_list))
+ directories.remove('')
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ for directory in directories:
+ os.makedirs(os.path.join(temp_dir, directory))
+
+ for path in complete_file_list:
+ shutil.copy2(path, os.path.join(temp_dir, path), follow_symlinks=False)
+
+ yield temp_dir
+
+
+def build(source_dir: str, tmp_dir: str) -> tuple[pathlib.Path, pathlib.Path]:
+ """Create a sdist and wheel."""
+ create = subprocess.run(
+ [sys.executable, '-m', 'build', '--no-isolation', '--outdir', tmp_dir],
+ stdin=subprocess.DEVNULL,
+ capture_output=True,
+ text=True,
+ check=False,
+ cwd=source_dir,
+ )
+
+ if create.returncode != 0:
+ raise RuntimeError(f'build failed:\n{create.stderr}\n{create.stdout}')
+
+ tmp_dir_files = list(pathlib.Path(tmp_dir).iterdir())
+
+ if len(tmp_dir_files) != 2:
+ raise RuntimeError(f'build resulted in {len(tmp_dir_files)} items instead of 2')
+
+ sdist_path = [path for path in tmp_dir_files if path.suffix == '.gz'][0]
+ wheel_path = [path for path in tmp_dir_files if path.suffix == '.whl'][0]
+
+ return sdist_path, wheel_path
+
+
+def list_sdist(path: pathlib.Path) -> list[str]:
+ """Return a list of the files in the sdist."""
+ item: tarfile.TarInfo
+
+ with tarfile.open(path) as sdist:
+ paths = ['/'.join(pathlib.Path(item.path).parts[1:]) for item in sdist.getmembers() if not item.isdir()]
+
+ return paths
+
+
+def list_wheel(path: pathlib.Path) -> list[str]:
+ """Return a list of the files in the wheel."""
+ with zipfile.ZipFile(path) as wheel:
+ paths = [item.filename for item in wheel.filelist if not item.is_dir()]
+
+ return paths
+
+
+def check_files(source: str, expected: list[str], actual: list[str]) -> list[str]:
+ """Verify the expected files exist and no extra files exist."""
+ missing = sorted(set(expected) - set(actual))
+ extra = sorted(set(actual) - set(expected))
+
+ errors = (
+ [f'{path}: missing from {source}' for path in missing] +
+ [f'{path}: unexpected in {source}' for path in extra]
+ )
+
+ return errors
+
+
+def main() -> None:
+ """Main program entry point."""
+ complete_file_list = sys.argv[1:] or sys.stdin.read().splitlines()
+
+ errors = []
+
+ # Limit visible files to those reported by ansible-test.
+ # This avoids including files which are not committed to git.
+ with clean_repository(complete_file_list) as clean_repo_dir:
+ if __version__.endswith('.dev0'):
+ # Make sure a changelog exists for this version when testing from devel.
+ # When testing from a stable branch the changelog will already exist.
+ major_minor_version = '.'.join(__version__.split('.')[:2])
+ changelog_path = f'changelogs/CHANGELOG-v{major_minor_version}.rst'
+ pathlib.Path(clean_repo_dir, changelog_path).touch()
+ complete_file_list.append(changelog_path)
+
+ expected_sdist_files = collect_sdist_files(complete_file_list)
+ expected_wheel_files = collect_wheel_files(complete_file_list)
+
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ sdist_path, wheel_path = build(clean_repo_dir, tmp_dir)
+
+ actual_sdist_files = list_sdist(sdist_path)
+ actual_wheel_files = list_wheel(wheel_path)
+
+ errors.extend(check_files('sdist', expected_sdist_files, actual_sdist_files))
+ errors.extend(check_files('wheel', expected_wheel_files, actual_wheel_files))
+
+ for error in errors:
+ print(error)
+
+
+if __name__ == '__main__':
+ main()