Skip to content
Open
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
22 changes: 22 additions & 0 deletions pytest_robotframework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,7 @@ def __init__(
log_pass: bool | None = None,
description: str | None = None,
fail_message: str | None = None,
verbosity: int | None = None,
) -> None:
super().__init__()
self.log_pass = log_pass
Expand Down Expand Up @@ -699,6 +700,27 @@ def __init__(
`AssertionError` message if the assertion fails. equivalent to a normal `assert` statement's
second argument"""

self.verbosiy = verbosity
"""override the verbosity for this assert statement. by default, `assert` statements will
not show the full diff unless pytest is run with `-vv` (aka. `--verbosity=2`). the values
correspond to the following pytest arguments:

- `--verbosity=-1` (aka. `-q` / `--quiet`)
- `--verbosity=0` (the default verbosity)
- `--verbosity=1` (aka. `-v` / `--verbose`)
- `--verbosity=2` (aka. `-vv`)

values higher than 2 are allowed but have no effect in pytest itself, however some plugins
might make use of higher verbosity.

`None` means the verbosity will be unchanged from whatever level pytest was run with

example:
-------
due to [this issue](https://github.com/pytest-dev/pytest/issues/12009#issuecomment-2201710676),
this argument has no effect when run from CI.
"""

@override
def __repr__(self) -> str:
"""make the custom fail message appear in the call to `AssertionError`"""
Expand Down
51 changes: 45 additions & 6 deletions pytest_robotframework/_internal/pytest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from robot.result.resultbuilder import ExecutionResult # pyright:ignore[reportUnknownVariableType]
from robot.run import RobotFramework, RobotSettings
from robot.utils.error import ErrorDetails
from typing_extensions import TYPE_CHECKING, Callable, Generator, Mapping, cast
from typing_extensions import TYPE_CHECKING, Callable, Generator, Mapping, TypeVar, cast

from pytest_robotframework import (
AssertOptions,
Expand Down Expand Up @@ -87,14 +87,22 @@
from types import TracebackType

from _pytest.terminal import TerminalReporter
from basedtyping import Fn
from pluggy import PluginManager
from pytest import CallInfo, Item, Parser, Session


_explanation_key = StashKey[str]()


def _call_assertion_hook(
def _assert_rewrite_context(fn: Fn) -> Fn:
"""make the function available within the context of a rewritten assert statement"""
setattr(rewrite, fn.__name__, fn)
return fn


@_assert_rewrite_context
def _call_assertion_hook( # pyright:ignore[reportUnusedFunction]
expression: str,
fail_message: object,
line_number: int,
Expand All @@ -120,9 +128,29 @@ def _call_assertion_hook(
)


# we aren't patching an existing function here but instead adding a new one to the rewrite module,
# since the rewritten assert statement needs to call it, and this is the easist way to do that
rewrite._call_assertion_hook = _call_assertion_hook # pyright:ignore[reportAttributeAccessIssue]
_original_verbosity = StashKey[int]()


@_assert_rewrite_context
def _change_verbosity(fail_message: object): # pyright:ignore[reportUnusedFunction]
if not isinstance(fail_message, AssertOptions) or fail_message.verbosiy is None:
return
item = current_item()
if not item:
return
item.stash[_original_verbosity] = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
_set_verbosity(item.config, fail_message.verbosiy)


def _change_verbosity_back():
item = current_item()
if not item:
return
original_verbosity = item.stash.get(_original_verbosity, None)
if original_verbosity is None:
return
_set_verbosity(item.config, original_verbosity)
del item.stash[_original_verbosity]


@patch_method(AssertionRewriter)
Expand All @@ -146,6 +174,9 @@ def visit_Assert( # noqa: N802
except StopIteration:
raise InternalError("failed to find if statement for assertion rewriting") from None
expression = _get_assertion_exprs(self.source)[assert_.lineno]
# call the verbosity override check right before the main test
result.insert(0, Expr(self.helper("_change_verbosity", assert_msg)))

# rice the fail statements:
raise_statement = cast(Raise, main_test.body.pop())
if not raise_statement.exc:
Expand Down Expand Up @@ -184,7 +215,9 @@ def visit_Assert( # noqa: N802
return result


HookWrapperResult = Generator[None, object, None]
_T_default_object = TypeVar("_T_default_object", default=object)

HookWrapperResult = Generator[None, _T_default_object, _T_default_object]


def _xdist_temp_dir(session: Session) -> Path:
Expand Down Expand Up @@ -374,6 +407,10 @@ def _robot_run_tests(session: Session, xdist_item: Item | None = None):
_ = _run_robot(session, robot_options)


def _set_verbosity(config: Config, verbosity: int):
config._inicache["verbosity_assertions"] = verbosity # pyright:ignore[reportPrivateUsage]


def pytest_addhooks(pluginmanager: PluginManager):
pluginmanager.add_hookspecs(hooks)

Expand Down Expand Up @@ -534,6 +571,8 @@ def pytest_robot_assertion(
if not assertion_error and fail_message.log_pass is not None:
show_in_log = fail_message.log_pass
description = fail_message.description
if fail_message.verbosiy is not None:
_change_verbosity_back()
else:
description = None
if show_in_log is None:
Expand Down
6 changes: 2 additions & 4 deletions scripts/clear_pycache.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from __future__ import annotations

from pathlib import Path

for path in Path().rglob("*.py[co]"):
path.unlink()
from shutil import rmtree

for path in Path().rglob("__pycache__"):
path.rmdir()
rmtree(path)
25 changes: 25 additions & 0 deletions tests/fixtures/test_python/test_assert_verbosity_overrides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from pytest import fixture

from pytest_robotframework import AssertOptions

# needed for fixtures
# pylint:disable=redefined-outer-name


@fixture
def big_value():
return list(range(6))


def test_default(big_value: list[int]):
assert big_value == []


def test_verbose(big_value: list[int]):
assert big_value == [], AssertOptions(verbosity=2)


def test_set_back_to_default(big_value: list[int]):
assert big_value == [1]
12 changes: 12 additions & 0 deletions tests/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,3 +959,15 @@ def test_maxfail(pr: PytestRobotTester):
"--maxfail doesn't work with xdist. https://github.com/pytest-dev/pytest-xdist/issues/868"
)
pr.run_and_assert_result("--maxfail=2", failed=2, skipped=1)


def test_assert_verbosity_overrides(pr: PytestRobotTester, monkeypatch: MonkeyPatch):
# https://github.com/pytest-dev/pytest/issues/12009#issuecomment-2201710676
monkeypatch.delenv("CI", raising=False)
pr.run_and_assert_result("-o", "enable_assertion_pass_hook=true", failed=3)
xml = output_xml()
assert xml.xpath("//test[@name='test_default']//msg[contains(., 'Use -v to get more diff')]")
assert xml.xpath("//test[@name='test_verbose']//msg[contains(., 'Full diff:')]")
assert xml.xpath(
"//test[@name='test_set_back_to_default']//msg[contains(., 'Use -v to get more diff')]"
)