#!/usr/bin/env python3 from dataclasses import dataclass from argparse import Namespace import argparse import os import shutil import subprocess from pathlib import Path # Run in repo root os.chdir(Path(__file__).parent.parent) @dataclass class Args(Namespace): force: bool nomeld: bool parser = argparse.ArgumentParser( description="Interactively create, fix, and remove *.out.expected files " "based on their corresponding *.out.produced files." ) parser.add_argument( "--force", "-f", action="store_true", help="automatically accept all suggested changes without user interaction", ) parser.add_argument( "--nomeld", "-M", action="store_true", help="diff files instead of using meld to merge them", ) args = parser.parse_args(namespace=Args) use_meld = not args.nomeld try: subprocess.run(["meld", "--version"], check=True, stdout=subprocess.DEVNULL) except (subprocess.CalledProcessError, FileNotFoundError): print("meld not found, falling back to --nomeld behavior") use_meld = False def prompt(message: str, options: str = "Yn") -> str: default: str | None = None for c in options: if c.isupper(): default = c.lower() break options = options.lower() options_display = "/".join(c.upper() if c == default else c for c in options) if args.force and default: print(f"{message} [{options_display}]: selected {default} due to --force") return default while True: response = input(f"{message} [{options_display}]: ").strip().lower() if not response and default: return default elif response in options.lower(): return response else: print(f"Please enter {options_display}.") def remove_file(file: Path, reason: str) -> None: print() print() print(f"{reason} but {file} exists.") if prompt(f"Remove {file}?") == "y": file.unlink() def compare_and_merge( produced_file: Path, expected_file: Path, ) -> None: produced = produced_file.read_bytes() expected = expected_file.read_bytes() if produced == expected: return print() print() print(f"{produced_file} differs from {expected_file}") if args.force: print(f"Automatically updating {expected_file} due to --force") shutil.copy(produced_file, expected_file) return if not use_meld: subprocess.run( [ "diff", "-au", "--strip-trailing-cr", "--color=always", expected_file, produced_file, ] ) if prompt(f"Apply diff to {expected_file}?") == "y": shutil.copy(produced_file, expected_file) return # This is the opposite direction of the tests' diff output, but meld puts # the cursor into the right file by default, and only saves the file with # the cursor when pressing Ctrl+S, so this order is more convenient for # quickly fixing many files. subprocess.run(["meld", produced_file, expected_file]) def create_or_ignore( produced_file: Path, expected_file: Path, ignored_file: Path, ) -> None: print() print() print(f"{produced_file} is not empty.") answer = prompt("Create expected file, ignore, or do nothing?", "Ein") if answer == "e": shutil.copy(produced_file, expected_file) elif answer == "i": ignored_file.touch() for produced_file in sorted(Path().rglob("*.out.produced")): expected_file = produced_file.with_suffix(".expected") ignored_file = produced_file.with_suffix(".ignored") no_test_file = produced_file.with_suffix("").with_suffix(".no_test") if no_test_file.exists(): if expected_file.exists(): remove_file(expected_file, f"{no_test_file} exists") if ignored_file.exists(): remove_file(ignored_file, f"{no_test_file} exists") continue produced = produced_file.read_bytes() if not produced: if expected_file.exists(): remove_file(expected_file, f"{produced_file} is empty") if ignored_file.exists(): remove_file(ignored_file, f"{produced_file} is empty") continue if ignored_file.exists(): if expected_file.exists(): remove_file(expected_file, f"{ignored_file} exists") continue if expected_file.exists(): compare_and_merge(produced_file, expected_file) else: create_or_ignore(produced_file, expected_file, ignored_file)