Skip to content

Commit a4ba332

Browse files
committed
rebase resolution
1 parent cd1dd7a commit a4ba332

1 file changed

Lines changed: 109 additions & 7 deletions

File tree

python/lib/sift_client/pytest_plugin.py

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,34 @@
2929

3030
REPORT_CONTEXT: Any = None
3131

32+
_PARAMETRIZE_PATH_KEY = pytest.StashKey[tuple[str, ...]]()
33+
# Each frame: (path_key, open step). Frames are shared across sibling test items
34+
# and drained at module-substep teardown / session end. Entries may be either a
35+
# real ``NewStep`` (online/offline) or a ``_NoopStep`` (disabled mode).
36+
_PARAMETRIZE_STACK: list[tuple[str, Any]] = []
37+
38+
39+
def _drain_parametrize_stack() -> None:
40+
while _PARAMETRIZE_STACK:
41+
_, ns = _PARAMETRIZE_STACK.pop()
42+
ns.__exit__(None, None, None)
43+
44+
45+
def _build_parametrize_path(item: pytest.Item) -> tuple[str, ...]:
46+
"""Outer-to-inner step display names for a parametrized item.
47+
48+
Pytest stores ``callspec.params`` with the BOTTOM decorator's axis first;
49+
the Sift step tree treats the TOP decorator as outermost, so we reverse.
50+
"""
51+
callspec = getattr(item, "callspec", None)
52+
if callspec is None or not callspec.params:
53+
return ()
54+
originalname = getattr(item, "originalname", item.name)
55+
frames: list[str] = [originalname]
56+
for name, value in reversed(callspec.params.items()):
57+
frames.append(f"{name}={value!r}")
58+
return tuple(frames)
59+
3260

3361
@dataclass(frozen=True)
3462
class _Option:
@@ -175,6 +203,18 @@ def pytest_configure(config: pytest.Config) -> None:
175203
)
176204

177205

206+
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
207+
"""Stash each item's parametrize path and cluster siblings by shared prefix."""
208+
for item in items:
209+
item.stash[_PARAMETRIZE_PATH_KEY] = _build_parametrize_path(item)
210+
items.sort(key=lambda i: i.stash[_PARAMETRIZE_PATH_KEY])
211+
212+
213+
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
214+
"""Drain any parametrize parents still open (e.g. when module_substep was gated off)."""
215+
_drain_parametrize_stack()
216+
217+
178218
def _is_offline(pytestconfig: pytest.Config | None) -> bool:
179219
return bool(_option_or_ini(pytestconfig, _OFFLINE))
180220

@@ -563,24 +603,75 @@ def report_context(
563603
def _step_impl(
564604
report_context: ReportContext | _NoopReportContext, request: pytest.FixtureRequest
565605
) -> Generator[NewStep | _NoopStep, None, None]:
566-
name = str(request.node.name)
567-
existing_docstring = request.node.obj.__doc__ or None
606+
node = request.node
607+
# Items get a parametrize path stashed in ``pytest_collection_modifyitems``;
608+
# modules/other nodes fall back to their node name. The leaf frame
609+
# (``path[-1]``) is the test-specific display name — parents are opened
610+
# by ``_parametrize_parents``.
611+
path = node.stash.get(_PARAMETRIZE_PATH_KEY, ())
612+
name = path[-1] if path else str(node.name)
613+
existing_docstring = node.obj.__doc__ or None
568614
with report_context.new_step(
569615
name=name, description=existing_docstring, assertion_as_fail_not_error=False
570616
) as new_step:
571617
yield new_step
572-
if hasattr(request.node, "rep_call") and request.node.rep_call.excinfo:
618+
if hasattr(node, "rep_call") and node.rep_call.excinfo:
573619
new_step.update_step_from_result(
574-
request.node.rep_call.excinfo,
575-
request.node.rep_call.excinfo.value,
576-
request.node.rep_call.excinfo.tb,
620+
node.rep_call.excinfo,
621+
node.rep_call.excinfo.value,
622+
node.rep_call.excinfo.tb,
577623
)
578624

579625

626+
@pytest.fixture(autouse=True)
627+
def _parametrize_parents(
628+
request: pytest.FixtureRequest,
629+
pytestconfig: pytest.Config,
630+
) -> None:
631+
"""Open/close shared parametrize parent steps for the current item.
632+
633+
Diffs the item's desired parametrize path against the open stack: pops the
634+
stale tail, then opens new parents (everything except the innermost frame —
635+
the ``step`` fixture creates that as the leaf). Parents persist across
636+
sibling items so a tree like ``test_x[a=1]`` / ``test_x[a=2]`` shares one
637+
``test_x`` container.
638+
639+
Gated off when the current item is excluded so that excluded items don't
640+
eagerly request ``report_context`` (which would defeat its lazy creation).
641+
Any parents still open at the end of a module are drained by
642+
``module_substep`` teardown; anything left at session end is drained by
643+
``pytest_sessionfinish``.
644+
"""
645+
default = bool(_option_or_ini(pytestconfig, _AUTOUSE))
646+
if not _sift_enabled_for(request.node, default):
647+
return None
648+
desired = request.node.stash.get(_PARAMETRIZE_PATH_KEY, ())
649+
parents = desired[:-1]
650+
common = 0
651+
while (
652+
common < len(_PARAMETRIZE_STACK)
653+
and common < len(parents)
654+
and _PARAMETRIZE_STACK[common][0] == parents[common]
655+
):
656+
common += 1
657+
while len(_PARAMETRIZE_STACK) > common:
658+
_, ns = _PARAMETRIZE_STACK.pop()
659+
ns.__exit__(None, None, None)
660+
if not parents[common:]:
661+
return None
662+
rc = request.getfixturevalue("report_context")
663+
for display in parents[common:]:
664+
ns = rc.new_step(name=display, assertion_as_fail_not_error=False)
665+
ns.__enter__()
666+
_PARAMETRIZE_STACK.append((display, ns))
667+
return None
668+
669+
580670
@pytest.fixture(autouse=True)
581671
def step(
582672
request: pytest.FixtureRequest,
583673
pytestconfig: pytest.Config,
674+
_parametrize_parents: None,
584675
) -> Generator[NewStep | _NoopStep | None, None, None]:
585676
"""Create an outer step for the function when the Sift gate is on.
586677
@@ -618,7 +709,18 @@ def module_substep(
618709
yield None
619710
return
620711
rc = request.getfixturevalue("report_context")
621-
yield from _step_impl(rc, request)
712+
gen = _step_impl(rc, request)
713+
new_step = next(gen)
714+
try:
715+
yield new_step
716+
finally:
717+
# Drain parametrize parents nested under this module step before it
718+
# exits — ReportContext.exit_step asserts the module step is the top.
719+
_drain_parametrize_stack()
720+
try:
721+
next(gen)
722+
except StopIteration:
723+
pass
622724

623725

624726
@pytest.fixture(scope="session")

0 commit comments

Comments
 (0)