mirror of
https://github.com/scylladb/scylladb.git
synced 2026-04-20 08:30:35 +00:00
158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
#
|
|
# Copyright (C) 2026-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
|
|
#
|
|
|
|
"""Pytest plugin for typed skip markers.
|
|
|
|
Framework-agnostic: the concrete skip types are provided by the
|
|
project's ``conftest.py`` via :class:`SkipReasonPlugin`.
|
|
|
|
Usage as decorator (after conftest.py registers the plugin)::
|
|
|
|
@pytest.mark.skip_bug(reason="scylladb/scylladb#12345")
|
|
@pytest.mark.skip_not_implemented(reason="no per tablet support yet")
|
|
|
|
Usage at runtime (inside test body or fixture) — prefer the
|
|
convenience wrappers from :mod:`test.pylib.skip_types`::
|
|
|
|
from test.pylib.skip_types import skip_env
|
|
skip_env("need --runveryslow option")
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import warnings
|
|
from collections.abc import Callable
|
|
from enum import StrEnum
|
|
|
|
import pytest
|
|
|
|
# StashKeys to carry skip metadata from collection to reporting.
|
|
SKIP_TYPE_KEY = pytest.StashKey[str]()
|
|
SKIP_REASON_KEY = pytest.StashKey[str]()
|
|
|
|
|
|
def skip(reason: str, skip_type: StrEnum):
|
|
"""Skip the current test at runtime with a typed reason.
|
|
|
|
Use instead of bare ``pytest.skip()``.
|
|
The *skip_type* should be a :class:`~enum.StrEnum` member defined
|
|
by the project (e.g. ``SkipType.SKIP_ENV``).
|
|
"""
|
|
pytest.skip(f"[{skip_type}] {reason}")
|
|
|
|
|
|
def skip_marker(item: pytest.Item, reason: str, skip_type: str) -> None:
|
|
"""Add a typed skip marker to an item during collection.
|
|
|
|
Use from ``pytest_collection_modifyitems`` hooks (e.g. runner.py's
|
|
``skip_mode`` processing) to skip a test with full type metadata.
|
|
The caller does not need to know how the metadata is stored.
|
|
"""
|
|
item.add_marker(pytest.mark.skip(reason=f"[{skip_type}] {reason}"))
|
|
item.stash[SKIP_TYPE_KEY] = skip_type
|
|
item.stash[SKIP_REASON_KEY] = reason
|
|
|
|
|
|
class SkipReasonPlugin:
|
|
"""Pytest plugin that converts typed skip markers into real skips
|
|
and enriches reports with skip metadata.
|
|
|
|
Args:
|
|
skip_types: A :class:`~enum.StrEnum` whose members define the
|
|
typed skip markers. Each member's ``marker_name`` property
|
|
becomes the pytest marker name, and its value becomes the
|
|
tag written to reports.
|
|
report_callback: Optional callback invoked with
|
|
``(skip_type, reason)`` for every skipped test that has
|
|
type metadata.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
skip_types: type[StrEnum],
|
|
*,
|
|
report_callback: Callable[[str, str], None] | None = None,
|
|
) -> None:
|
|
self._skip_types = skip_types
|
|
self._report_callback = report_callback
|
|
|
|
@staticmethod
|
|
def _get_reason(mark: pytest.Mark) -> str:
|
|
"""Extract reason from a marker (keyword or positional)."""
|
|
return mark.kwargs.get("reason") or (mark.args[0] if mark.args else "")
|
|
|
|
@staticmethod
|
|
def _parse_skip_type(longrepr) -> tuple[str, str] | None:
|
|
"""Try to extract ``(skip_type, reason)`` from a ``[type] reason`` message.
|
|
|
|
Returns ``None`` when *longrepr* does not match the expected format.
|
|
"""
|
|
text = longrepr[-1] if isinstance(longrepr, tuple) else str(longrepr)
|
|
text = text.removeprefix("Skipped: ")
|
|
if text.startswith("[") and "]" in text:
|
|
tag, _, reason = text[1:].partition("]")
|
|
return tag, reason.lstrip()
|
|
return None
|
|
|
|
@pytest.hookimpl(trylast=True)
|
|
def pytest_collection_modifyitems(self, items: list[pytest.Item]) -> None:
|
|
"""Apply typed markers and warn on bare skips."""
|
|
for item in items:
|
|
# Convert typed skip markers to real pytest.mark.skip.
|
|
for st in self._skip_types:
|
|
for mark in item.iter_markers(st.marker_name):
|
|
reason = self._get_reason(mark)
|
|
if not reason:
|
|
raise pytest.UsageError(
|
|
f"Marker @pytest.mark.{st.marker_name} on {item.nodeid} "
|
|
f"requires a 'reason' argument."
|
|
)
|
|
item.add_marker(pytest.mark.skip(reason=f"[{st}] {reason}"))
|
|
item.stash[SKIP_TYPE_KEY] = str(st)
|
|
item.stash[SKIP_REASON_KEY] = reason
|
|
|
|
# Warn on bare pytest.mark.skip not added by typed markers.
|
|
# skip_mode sets SKIP_TYPE_KEY before this hook runs (trylast).
|
|
if SKIP_TYPE_KEY not in item.stash:
|
|
bare = [self._get_reason(m) for m in item.iter_markers("skip")]
|
|
if bare:
|
|
alternatives = ", ".join(
|
|
f"@pytest.mark.{st.marker_name}" for st in self._skip_types)
|
|
# TODO: Change to pytest.fail() after full migration.
|
|
warnings.warn(
|
|
f"Untyped skip on {item.nodeid}: {'; '.join(bare)}. "
|
|
f"Use {alternatives} instead.",
|
|
)
|
|
|
|
@pytest.hookimpl(hookwrapper=True)
|
|
def pytest_runtest_makereport(self, item: pytest.Item):
|
|
"""Enrich JUnit XML reports (and optionally custom reporters) with skip metadata."""
|
|
outcome = yield
|
|
report = outcome.get_result()
|
|
|
|
if not report.skipped:
|
|
return
|
|
|
|
skip_type = item.stash.get(SKIP_TYPE_KEY, None)
|
|
reason = item.stash.get(SKIP_REASON_KEY, "")
|
|
|
|
# Runtime skips (via skip()) don't set stash keys — parse from message.
|
|
if skip_type is None and report.longrepr:
|
|
parsed = self._parse_skip_type(report.longrepr)
|
|
if parsed is None:
|
|
return
|
|
skip_type, reason = parsed
|
|
|
|
if skip_type is None:
|
|
return
|
|
|
|
item.user_properties.append(("skip_type", skip_type))
|
|
if reason:
|
|
item.user_properties.append(("skip_reason", reason))
|
|
|
|
if self._report_callback is not None:
|
|
self._report_callback(skip_type, reason)
|