175 lines
5.5 KiB
Python
175 lines
5.5 KiB
Python
|
|
from typing import List
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from _pytest.config import Config
|
||
|
|
from _pytest.config.argparsing import Parser
|
||
|
|
from _pytest.main import Session
|
||
|
|
from _pytest.mark import Mark
|
||
|
|
from _pytest.python import Function
|
||
|
|
|
||
|
|
from .sorter import Sorter
|
||
|
|
|
||
|
|
|
||
|
|
def pytest_configure(config: Config) -> None:
|
||
|
|
"""
|
||
|
|
Register the "order" marker and configure the plugin,
|
||
|
|
depending on the CLI options.
|
||
|
|
"""
|
||
|
|
|
||
|
|
provided_by_pytest_order = (
|
||
|
|
"Provided by pytest-order. " "See also: https://pytest-order.readthedocs.io/"
|
||
|
|
)
|
||
|
|
|
||
|
|
config_line = (
|
||
|
|
"order: specify ordering information for when tests should run "
|
||
|
|
"in relation to one another. " + provided_by_pytest_order
|
||
|
|
)
|
||
|
|
config.addinivalue_line("markers", config_line)
|
||
|
|
# We need to dynamically add this `tryfirst` decorator to the plugin:
|
||
|
|
# only when the CLI option is present should the decorator be added.
|
||
|
|
# Thus, we manually run the decorator on the class function and
|
||
|
|
# manually replace it.
|
||
|
|
if config.getoption("indulgent_ordering"):
|
||
|
|
wrapper = pytest.hookimpl(tryfirst=True)
|
||
|
|
else:
|
||
|
|
wrapper = pytest.hookimpl(trylast=True)
|
||
|
|
OrderingPlugin.pytest_collection_modifyitems = wrapper( # type:ignore[attr-defined]
|
||
|
|
modify_items
|
||
|
|
)
|
||
|
|
config.pluginmanager.register(OrderingPlugin(), "orderingplugin")
|
||
|
|
|
||
|
|
|
||
|
|
def pytest_addoption(parser: Parser) -> None:
|
||
|
|
"""Set up CLI option for pytest"""
|
||
|
|
group = parser.getgroup("order")
|
||
|
|
group.addoption(
|
||
|
|
"--indulgent-ordering",
|
||
|
|
action="store_true",
|
||
|
|
dest="indulgent_ordering",
|
||
|
|
help=(
|
||
|
|
"Request that the sort order provided by pytest-order be applied "
|
||
|
|
"before other sorting, allowing the other sorting to have priority"
|
||
|
|
),
|
||
|
|
)
|
||
|
|
group.addoption(
|
||
|
|
"--order-scope",
|
||
|
|
action="store",
|
||
|
|
dest="order_scope",
|
||
|
|
help=(
|
||
|
|
"Defines the scope used for ordering. Possible values are: "
|
||
|
|
"'session' (default), 'module', and 'class'. "
|
||
|
|
"Ordering is only done inside a scope."
|
||
|
|
),
|
||
|
|
)
|
||
|
|
group.addoption(
|
||
|
|
"--order-scope-level",
|
||
|
|
action="store",
|
||
|
|
type=int,
|
||
|
|
dest="order_scope_level",
|
||
|
|
help=(
|
||
|
|
"Defines that the given directory level is used as order scope. "
|
||
|
|
"Cannot be used with --order-scope. The value is a number "
|
||
|
|
"that defines the hierarchical index of the directories used as "
|
||
|
|
"order scope, starting with 0 at session scope."
|
||
|
|
),
|
||
|
|
)
|
||
|
|
group.addoption(
|
||
|
|
"--order-group-scope",
|
||
|
|
action="store",
|
||
|
|
dest="order_group_scope",
|
||
|
|
help=(
|
||
|
|
"Defines the scope used for order groups. Possible values are: "
|
||
|
|
" 'session' (default), 'module', and 'class'. "
|
||
|
|
"Ordering is first done inside a group, then between groups."
|
||
|
|
),
|
||
|
|
)
|
||
|
|
group.addoption(
|
||
|
|
"--sparse-ordering",
|
||
|
|
action="store_true",
|
||
|
|
dest="sparse_ordering",
|
||
|
|
help=(
|
||
|
|
"If there are gaps between ordinals, they are filled "
|
||
|
|
"with unordered tests."
|
||
|
|
),
|
||
|
|
)
|
||
|
|
group.addoption(
|
||
|
|
"--order-dependencies",
|
||
|
|
action="store_true",
|
||
|
|
dest="order_dependencies",
|
||
|
|
help=(
|
||
|
|
"If set, dependencies added by pytest-dependency will be ordered "
|
||
|
|
"if needed."
|
||
|
|
),
|
||
|
|
)
|
||
|
|
group.addoption(
|
||
|
|
"--order-marker-prefix",
|
||
|
|
action="store",
|
||
|
|
dest="order_marker_prefix",
|
||
|
|
help=(
|
||
|
|
"If set, markers starting with the given prefix followed by a number "
|
||
|
|
"are handled like order markers with an index."
|
||
|
|
),
|
||
|
|
)
|
||
|
|
group.addoption(
|
||
|
|
"--error-on-failed-ordering",
|
||
|
|
action="store_true",
|
||
|
|
dest="error_on_failed_ordering",
|
||
|
|
help=(
|
||
|
|
"If set, tests with relative markers that could not be ordered "
|
||
|
|
"will error instead of generating only a warning."
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _get_mark_description(mark: Mark):
|
||
|
|
if mark.kwargs:
|
||
|
|
return ", ".join([f"{k}={v}" for k, v in mark.kwargs.items()])
|
||
|
|
elif mark.args:
|
||
|
|
return f"index={mark.args[0]}"
|
||
|
|
return mark
|
||
|
|
|
||
|
|
|
||
|
|
def pytest_generate_tests(metafunc):
|
||
|
|
"""
|
||
|
|
Handle multiple pytest.mark.order decorators.
|
||
|
|
|
||
|
|
Make parametrized tests with corresponding order marks.
|
||
|
|
"""
|
||
|
|
if getattr(metafunc, "function", False):
|
||
|
|
if getattr(metafunc.function, "pytestmark", False):
|
||
|
|
# Get list of order marks
|
||
|
|
marks = metafunc.function.pytestmark
|
||
|
|
order_marks = [mark for mark in marks if mark.name == "order"]
|
||
|
|
if len(order_marks) > 1:
|
||
|
|
# Remove all order marks
|
||
|
|
metafunc.function.pytestmark = [
|
||
|
|
mark for mark in marks if mark.name != "order"
|
||
|
|
]
|
||
|
|
# Prepare arguments for parametrization with order marks
|
||
|
|
args = [
|
||
|
|
pytest.param(_get_mark_description(mark), marks=[mark])
|
||
|
|
for mark in order_marks
|
||
|
|
]
|
||
|
|
if "order" not in metafunc.fixturenames:
|
||
|
|
metafunc.fixturenames.append("order")
|
||
|
|
metafunc.parametrize("order", args)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def fail_after_cannot_order():
|
||
|
|
pytest.fail("The test could not be ordered")
|
||
|
|
|
||
|
|
|
||
|
|
class OrderingPlugin:
|
||
|
|
"""
|
||
|
|
Plugin implementation.
|
||
|
|
|
||
|
|
By putting this in a class, we are able to dynamically register it after
|
||
|
|
the CLI is parsed.
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def modify_items(session: Session, config: Config, items: List[Function]) -> None:
|
||
|
|
sorter = Sorter(config, items)
|
||
|
|
items[:] = sorter.sort_items()
|