diff --git a/decoy/__init__.py b/decoy/__init__.py index dc51f0e..571bd56 100644 --- a/decoy/__init__.py +++ b/decoy/__init__.py @@ -5,6 +5,9 @@ from .core import DecoyCore, StubCore from .types import ClassT, FuncT, ReturnT +# ensure decoy does not pollute pytest tracebacks +__tracebackhide__ = True + class Decoy: """Decoy test double state container.""" diff --git a/decoy/core.py b/decoy/core.py index c8b17a3..e0d500c 100644 --- a/decoy/core.py +++ b/decoy/core.py @@ -10,6 +10,9 @@ from .warning_checker import WarningChecker from .types import ReturnT +# ensure decoy.core does not pollute Pytest tracebacks +__tracebackhide__ = True + class DecoyCore: """The DecoyCore class implements the main logic of Decoy.""" diff --git a/decoy/errors.py b/decoy/errors.py index 2037357..511279b 100644 --- a/decoy/errors.py +++ b/decoy/errors.py @@ -60,7 +60,7 @@ def __init__( heading=heading, rehearsals=rehearsals, calls=calls, - include_calls=times is None, + include_calls=times is None or times == len(calls), ) super().__init__(message) diff --git a/decoy/stringify.py b/decoy/stringify.py index a0f837a..862968b 100644 --- a/decoy/stringify.py +++ b/decoy/stringify.py @@ -31,6 +31,11 @@ def count(count: int, noun: str) -> str: return f"{count} {noun}{'s' if count != 1 else ''}" +def join_lines(*lines: str) -> str: + """Join a list of lines with newline characters.""" + return os.linesep.join(lines).strip() + + def stringify_error_message( heading: str, rehearsals: Sequence[BaseSpyRehearsal], @@ -38,14 +43,12 @@ def stringify_error_message( include_calls: bool = True, ) -> str: """Stringify an error message about a rehearsals to calls comparison.""" - return os.linesep.join( - [ - heading, - stringify_call_list(rehearsals), - ( - f"Found {count(len(calls), 'call')}" - f"{'.' if len(calls) == 0 or not include_calls else ':'}" - ), - stringify_call_list(calls) if include_calls else "", - ] - ).strip() + return join_lines( + heading, + stringify_call_list(rehearsals), + ( + f"Found {count(len(calls), 'call')}" + f"{'.' if len(calls) == 0 or not include_calls else ':'}" + ), + stringify_call_list(calls) if include_calls else "", + ) diff --git a/decoy/verifier.py b/decoy/verifier.py index dcc4e60..2ce01c0 100644 --- a/decoy/verifier.py +++ b/decoy/verifier.py @@ -4,6 +4,9 @@ from .spy_calls import SpyCall, VerifyRehearsal, match_call from .errors import VerifyError +# ensure decoy.verifier does not pollute Pytest tracebacks +__tracebackhide__ = True + class Verifier: """An interface to verify that spies were called as expected.""" diff --git a/tests/test_errors.py b/tests/test_errors.py index d3c586d..0f74227 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -92,6 +92,23 @@ class VerifyErrorSpec(NamedTuple): ] ), ), + VerifyErrorSpec( + rehearsals=[ + VerifyRehearsal(spy_id=101, spy_name="spy_101", args=(1, 2, 3), kwargs={}), + ], + calls=[ + SpyCall(spy_id=101, spy_name="spy_101", args=(4, 5, 6), kwargs={}), + ], + times=1, + expected_message=os.linesep.join( + [ + "Expected exactly 1 call:", + "1.\tspy_101(1, 2, 3)", + "Found 1 call:", + "1.\tspy_101(4, 5, 6)", + ] + ), + ), ]