Ctrl K
ComunidadGuías…

scripts/pytest_shard.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Explicit, deterministic pytest shards for the exhaustive CI gate.

Run ``python -m pytest -p scripts.pytest_shard`` from the repository root and
provide both ``--ft-shard-index`` and ``--ft-shard-count``. Loading the plugin
without either option leaves collection unchanged. Each shard must use the
same collection arguments.
"""

from __future__ import annotations

import hashlib

import pytest


def shard_for_nodeid(nodeid: str, count: int) -> int:
    """Return a node ID's stable, zero-based shard for a positive shard count.

    Pytest node IDs use repository-relative paths with forward slashes. Hash
    their UTF-8 bytes rather than Python's process-randomised ``hash()`` so
    membership is independent of collection order, worker and platform.
    """
    if count < 1:
        raise ValueError("shard count must be positive")
    digest = hashlib.sha256(nodeid.encode("utf-8")).digest()
    return int.from_bytes(digest, "big") % count


def pytest_addoption(parser: pytest.Parser) -> None:
    """Register opt-in shard arguments without changing the default run."""
    group = parser.getgroup("FlintTrade sharding")
    group.addoption(
        "--ft-shard-index",
        type=int,
        default=None,
        metavar="INDEX",
        help="Zero-based shard index; requires --ft-shard-count.",
    )
    group.addoption(
        "--ft-shard-count",
        type=int,
        default=None,
        metavar="COUNT",
        help="Positive shard count; requires --ft-shard-index.",
    )


def _shard_settings(config: pytest.Config) -> tuple[int, int] | None:
    index: int | None = config.getoption("ft_shard_index")
    count: int | None = config.getoption("ft_shard_count")
    if index is None and count is None:
        return None
    if index is None or count is None:
        raise pytest.UsageError("--ft-shard-index and --ft-shard-count must be supplied together")
    if count < 1:
        raise pytest.UsageError("--ft-shard-count must be positive")
    if not 0 <= index < count:
        raise pytest.UsageError("--ft-shard-index must be at least 0 and less than --ft-shard-count")
    return index, count


def pytest_configure(config: pytest.Config) -> None:
    """Reject invalid arguments before collecting or executing tests."""
    _shard_settings(config)


@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
    """Keep this shard's items, preserving other plugins' selection and order."""
    settings = _shard_settings(config)
    if settings is None:
        return
    index, count = settings
    selected: list[pytest.Item] = []
    deselected: list[pytest.Item] = []
    for item in items:
        target = selected if shard_for_nodeid(item.nodeid, count) == index else deselected
        target.append(item)
    items[:] = selected
    if deselected:
        config.hook.pytest_deselected(items=deselected)
    # Leave an empty shard to pytest's normal NO_TESTS_COLLECTED exit status.