import filecmp import os import pathlib from typing import Callable, List, Optional from typing_extensions import override from approval_utilities.utilities.multiline_string_utils import remove_indentation_from from approvaltests.core.comparator import Comparator from approvaltests.core.namer import Namer from approvaltests.core.reporter import Reporter from approvaltests.core.writer import Writer from approvaltests.internals.logs.approved_file_log import ApprovedFilesLog from approvaltests.internals.logs.failed_comparison_log import FailedComparisonLog def exists(path: str) -> bool: return os.path.isfile(path) class ReporterNotWorkingException(Exception): def __init__(self, reporter: Reporter): super().__init__(f"Reporter {reporter} failed to work!") class FileComparator(Comparator): @override def compare(self, received_path: str, approved_path: str) -> bool: if not exists(approved_path) or not exists(received_path): return False if filecmp.cmp(approved_path, received_path, shallow=False): return True try: approved_raw = pathlib.Path(approved_path).read_text() approved_text = approved_raw.replace("\r\n", "\n") received_raw = pathlib.Path(received_path).read_text() received_text = received_raw.replace("\r\n", "\n") return approved_text == received_text except BaseException: return False class FileApprover: previous_approved: List[str] = [] allowed_duplicates: List[Callable[[str], bool]] = [] @staticmethod def verify( namer: Namer, writer: Writer, reporter: Reporter, comparator: Comparator ) -> Optional[str]: approved = namer.get_approved_filename() received = namer.get_received_filename() if FileApprover.is_this_a_multiple_verify(approved): return FileApprover.get_duplicate_verify_error_message(approved) FileApprover.previous_approved.append(approved) # The writer has the ability to change the name of the received file received = writer.write_received_file(received) verified = FileApprover.verify_files(approved, received, reporter, comparator) if not verified: return ( f"Approval Mismatch, received != approved\n" f"\tApproved: {approved}\n" f"\tReceived: {received} " ) return None @staticmethod def get_duplicate_verify_error_message(approved: str) -> str: return remove_indentation_from( f""" We noticed that you called verify more than once in the same test. This is the second call to verify: approved_file: {approved} By default, ApprovalTests only allows one verify() call per test. To find out more, visit: https://github.com/approvals/ApprovalTests.Python/blob/main/docs/how_to/multiple_approvals_per_test.md # Possible Fixes 1. Separate your test into two tests 2. In your verify call, add `options=NamerFactory.with_parameters("your_paramater")` 3. In your test, call `approvals.settings().allow_multiple_verify_calls_for_this_method()` """ ) @staticmethod def is_this_a_multiple_verify(approved: str) -> bool: return ( approved in FileApprover.previous_approved and not FileApprover.is_duplicate_allowed(approved) ) @staticmethod def is_duplicate_allowed(approved: str) -> bool: for allowed in FileApprover.allowed_duplicates: if allowed(approved): return True return False @staticmethod def verify_files( approved_file: str, received_file: str, reporter: Reporter, comparator: Comparator, ) -> bool: ApprovedFilesLog.log(approved_file) if comparator.compare(received_file, approved_file): os.remove(received_file) return True FailedComparisonLog.log(received_file, approved_file) worked = reporter.report(received_file, approved_file) if not worked: raise ReporterNotWorkingException(reporter) return False @staticmethod def add_allowed_duplicates(is_duplicate_allowed: Callable[[str], bool]) -> None: FileApprover.allowed_duplicates.append(is_duplicate_allowed)