Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 109 additions & 4 deletions craft_cli/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,31 @@ def _check(
expected_text: str,
method_name: str,
regex: bool, # noqa: FBT001
*,
invert: bool = False,
**kwargs: Any,
) -> Any:
"""Really verify messages."""
if regex:
expected_text = _RegexComparingText(expected_text)
expected_call = call(method_name, expected_text, **kwargs)
found = None
for stored_call in self.interactions:
if stored_call == expected_call:
return stored_call.args[1]
raise AssertionError(
f"Expected call {expected_call} not found in {self.interactions}"
)
found = stored_call.args[1]
break

if invert and found is not None:
raise AssertionError(
f"Found call {expected_call} when it should have been absent from {self.interactions}."
Comment thread
bepri marked this conversation as resolved.
)

if not invert and found is None:
raise AssertionError(
f"Expected call {expected_call} not found in {self.interactions}"
)

return found

def assert_message(
self,
Expand All @@ -186,6 +199,20 @@ def assert_message(
"""
return self._check(expected_text, "message", regex)

def assert_no_message(
self,
expected_text: str,
*,
regex: bool = False,
) -> Any:
"""Check the 'message' method was not used.

It verifies that the method was never called with the expected text.

If 'regex' is True, the expected text will be used as a regular expression.
"""
return self._check(expected_text, "message", regex, invert=True)
Comment thread
bepri marked this conversation as resolved.

def assert_progress(
self,
expected_text: str,
Expand All @@ -205,6 +232,28 @@ def assert_progress(
result = self._check(expected_text, "progress", regex, permanent=permanent)
return result

def assert_no_progress(
self,
expected_text: str,
permanent: bool | None = None, # noqa: FBT001
*,
regex: bool = False,
) -> Any:
"""Check the 'progress' method was not used.

It verifies that the method was never called with the expected text (with
the given 'permanent' flag).

If 'regex' is True, the expected text will be used as a regular expression.
"""
if permanent is None:
result = self._check(expected_text, "progress", regex, invert=True)
else:
result = self._check(
expected_text, "progress", regex, permanent=permanent, invert=True
)
return result

def assert_verbose(
self,
expected_text: str,
Expand All @@ -218,6 +267,20 @@ def assert_verbose(
"""
return self._check(expected_text, "verbose", regex)

def assert_no_verbose(
self,
expected_text: str,
*,
regex: bool = False,
) -> Any:
"""Check the 'verbose' method was not used.

It verifies that the method was never called with the expected text.

If 'regex' is True, the expected text will be used as a regular expression.
"""
return self._check(expected_text, "verbose", regex, invert=True)

def assert_warning(self, expected_text: str, *, regex: bool = False) -> Any:
"""Check the 'warning' method was properly used.

Expand All @@ -227,6 +290,20 @@ def assert_warning(self, expected_text: str, *, regex: bool = False) -> Any:
"""
return self._check(expected_text, "warning", regex)

def assert_no_warning(
self,
expected_text: str,
*,
regex: bool = False,
) -> Any:
"""Check the 'warning' method was not used.

It verifies that the method was never called with the expected text.

If 'regex' is True, the expected text will be used as a regular expression.
"""
return self._check(expected_text, "warning", regex, invert=True)

def assert_debug(
self,
expected_text: str,
Expand All @@ -240,6 +317,20 @@ def assert_debug(
"""
return self._check(expected_text, "debug", regex)

def assert_no_debug(
self,
expected_text: str,
*,
regex: bool = False,
) -> Any:
"""Check the 'debug' method was not used.

It verifies that the method was never called with the expected text.

If 'regex' is True, the expected text will be used as a regular expression.
"""
return self._check(expected_text, "debug", regex, invert=True)

def assert_trace(
self,
expected_text: str,
Expand All @@ -253,6 +344,20 @@ def assert_trace(
"""
return self._check(expected_text, "trace", regex)

def assert_no_trace(
self,
expected_text: str,
*,
regex: bool = False,
) -> Any:
"""Check the 'trace' method was not used.

It verifies that the method was never called with the expected text.

If 'regex' is True, the expected text will be used as a regular expression.
"""
return self._check(expected_text, "trace", regex, invert=True)

def assert_messages(self, texts: list[str]) -> None:
"""Check that the 'message' method was called several times with the given texts.

Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ included in each version.

New features:

- Add negative assertion methods to the ``emitter`` test fixture, such as
:py:meth:`.RecordingEmitter.assert_no_message`, to verify that a message was
never emitted.
- Add an optional ``pytest`` dependency group (``craft-cli[pytest]``) to track
the minimum pytest version required by the ``emitter`` test plugin.
- Add a :py:func:`~craft_cli.pytest_plugin.raises_craft_error` helper to assert
Expand Down
27 changes: 27 additions & 0 deletions tests/test_pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,33 @@ def test_emitter_record_trace_regex(emitter):
emitter.assert_trace("[fx]oo.*", regex=True)


@pytest.mark.parametrize(
"emit_method", ["message", "progress", "verbose", "warning", "debug", "trace"]
)
def test_emitter_record_no_call_plain(emitter, emit_method):
"""Can verify that a call was never made."""
emit_fn = getattr(messages.emit, emit_method)
assert_fn = getattr(emitter, f"assert_no_{emit_method}")

emit_fn("something else we don't care about")
assert_fn("foobar")

emit_fn("foobar")
with pytest.raises(AssertionError):
assert_fn("foobar")


def test_emitter_record_no_progress_permanent(emitter):
"""Can verify that `progress` was never called with the given permanent flag."""
messages.emit.progress("foobar", permanent=True)

emitter.assert_no_progress("foobar", permanent=False)
# no permanent filter means only matching calls without the flag
emitter.assert_no_progress("foobar")
with pytest.raises(AssertionError):
emitter.assert_no_progress("foobar", permanent=True)


def test_emitter_record_progress_bar_ok(emitter):
"""Calls to `progress_bar` are recorded."""
with messages.emit.progress_bar("title", 20, delta=True) as progress_bar:
Expand Down
Loading