blob: e1885806bbd1918e5ec5aed7cfd89537c8f7d0a6 [file] [log] [blame]
#!/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())