| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2024 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """An integration test for generate_repository.py. |
| |
| This invokes generate_repository.py with a fixed input IDK and fake Ninja |
| output directory, and compare the result with a fixed golden output IDK |
| directory. |
| |
| See the `validation_data/README.md` file for details. |
| """ |
| |
| import argparse |
| import difflib |
| import filecmp |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import typing as T |
| from pathlib import Path |
| |
| _SCRIPT_DIR = Path(__file__).parent |
| |
| |
| def _get_files_from(top_dir: Path) -> set[str]: |
| """Walk the top_dir directory, and return a set of relative paths to all its files.""" |
| return { |
| os.path.relpath(os.path.join(dirpath, filename), top_dir) |
| for dirpath, dirnames, filenames in os.walk(top_dir) |
| for filename in filenames |
| } |
| |
| |
| def compare_directories( |
| left_dir: Path, right_dir: Path |
| ) -> T.Tuple[T.Sequence[str], T.Sequence[str], T.Sequence[str]]: |
| """Compare the content of two directories. |
| |
| Args: |
| left: Path to first directory. |
| right: Path to second directory. |
| Returns: |
| A 3-tuple whose item correspond to the following lists or |
| relative path strings: |
| |
| - different_files: files that are present in both directories, |
| but whose content differs. |
| - left_only_file: files that only appear in the `left` directory. |
| - right_only_files: files that only appear in the `right` directory. |
| """ |
| left_files = _get_files_from(left_dir) |
| right_files = _get_files_from(right_dir) |
| |
| left_only_files = left_files - right_files |
| right_only_files = right_files - left_files |
| |
| # Find files that are different. |
| different_files = set() |
| common_files = left_files & right_files |
| for file in common_files: |
| left = left_dir / file |
| right = right_dir / file |
| if left.is_symlink() != right.is_symlink(): |
| different_files.add(file) |
| continue |
| |
| if left.is_symlink(): |
| if ( |
| not right.is_symlink() |
| or (right.parent / right.readlink()).resolve() |
| != (left.parent / left.readlink()).resolve() |
| ): |
| different_files.add(file) |
| continue |
| |
| if not filecmp.cmp(left, right, shallow=False): |
| different_files.add(file) |
| continue |
| |
| return ( |
| sorted(different_files), |
| sorted(left_only_files), |
| sorted(right_only_files), |
| ) |
| |
| |
| class BuildDir(object): |
| """Convenience class to model either a build directory.""" |
| |
| def __init__(self, build_dir: None | Path = None) -> None: |
| """Create new instance. |
| |
| Args: |
| build_dir: Path to a build directory, or None, in which cases |
| a temporary directory will be created, whose content will |
| be cleaned up automatically when this instance is freed. |
| """ |
| if build_dir: |
| self._top_dir = build_dir |
| if self._top_dir.exists(): |
| shutil.rmtree(self._top_dir, ignore_errors=True) |
| self._top_dir.mkdir(parents=True) |
| else: |
| self._tmp_dir = tempfile.TemporaryDirectory( |
| prefix="generate_idk_repository-integration-" |
| ) |
| self._top_dir = Path(self._tmp_dir.name) |
| |
| @property |
| def path(self) -> Path: |
| return self._top_dir |
| |
| |
| def main() -> int: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "--validation_data-dir", |
| type=Path, |
| default=_SCRIPT_DIR / "validation_data", |
| help="Specify alternative validation_data/ directory.", |
| ) |
| parser.add_argument( |
| "--build-dir", |
| type=Path, |
| help="Optional build directory used for development. Useful to see what was generated and compare it with the expected IDK output.", |
| ) |
| parser.add_argument( |
| "--stamp-file", type=Path, help="Output stamp file path." |
| ) |
| parser.add_argument( |
| "--quiet", action="store_true", help="Do not print anything on success." |
| ) |
| parser.add_argument( |
| "--no-diff-check", |
| action="store_true", |
| default=False, |
| help="Disable diff check, for development.", |
| ) |
| parser.add_argument( |
| "--hermetic-inputs-file", |
| type=Path, |
| help="Output file path to write the list of implicit inputs for this script.", |
| ) |
| |
| args = parser.parse_args() |
| |
| if args.hermetic_inputs_file: |
| # Handle --hermetic_inputs_file here, results must be relative |
| # to the current directory. |
| implicit_inputs = [] |
| for root, subdirs, subfiles in os.walk(args.validation_data_dir): |
| for subfile in subfiles: |
| implicit_inputs.append( |
| os.path.relpath(os.path.join(root, subfile)) |
| ) |
| |
| args.hermetic_inputs_file.parent.mkdir(parents=True, exist_ok=True) |
| args.hermetic_inputs_file.write_text("\n".join(sorted(implicit_inputs))) |
| return 0 |
| |
| build_dir = BuildDir(args.build_dir) |
| |
| # First, copy the source test_data tree to a temporary directory. |
| top_dir = build_dir.path |
| shutil.copytree( |
| args.validation_data_dir, top_dir, symlinks=True, dirs_exist_ok=True |
| ) |
| |
| generate_repository_script = _SCRIPT_DIR / "generate_repository.py" |
| |
| # Second, run the script to generate output_idk |
| ret = subprocess.run( |
| [ |
| sys.executable, |
| "-S", |
| str(generate_repository_script), |
| "--repository-name", |
| "test_idk", |
| "--input-dir", |
| f"{top_dir}/input_idk", |
| "--ninja-build-dir", |
| f"{top_dir}/ninja_artifacts", |
| "--output-dir", |
| f"{top_dir}/output_idk", |
| ] |
| ) |
| ret.check_returncode() |
| |
| output_idk = top_dir / "output_idk" |
| expected_idk = top_dir / "expected_idk" |
| |
| if not args.no_diff_check: |
| diff_files, left_only, right_only = compare_directories( |
| expected_idk, output_idk |
| ) |
| |
| errors: list[str] = [] |
| if left_only: |
| errors.append( |
| "Missing files from the output IDK:\n %s\n" |
| % ("\n ".join(sorted(left_only))) |
| ) |
| if right_only: |
| errors.append( |
| "Unexpected files in the output IDK:\n %s\n" |
| % ("\n ".join(sorted(right_only))) |
| ) |
| if diff_files: |
| errors.append("Differences between golden and output IDK:") |
| for file_name in diff_files: |
| left_file = expected_idk / file_name |
| right_file = output_idk / file_name |
| if not left_file.exists(): |
| errors.append(f"< INVALID SYMLINK {left_file}") |
| if not right_file.exists(): |
| errors.append(f"> INVALID SYMLINK {right_file}") |
| if left_file.exists() and right_file.exists(): |
| diff_lines = difflib.unified_diff( |
| left_file.read_text().splitlines(), |
| right_file.read_text().splitlines(), |
| f"expected_idk/{file_name}", |
| f"output_idk/{file_name}", |
| lineterm="", |
| ) |
| for line in diff_lines: |
| errors.append(" " + line) |
| |
| if errors: |
| print("ERRORS:\n" + "\n".join(errors), file=sys.stderr) |
| return 1 |
| |
| if args.stamp_file: |
| args.stamp_file.write_text("") |
| |
| if not args.quiet: |
| print("OK") |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |