diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index fd5b2173e9..b7b8736ecf 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -234,8 +234,8 @@ jobs: # timings, uploaded by the last step of this job. # # #1958, the test tiers: tests/_tiers.py's module_tier() puts every test - # module in `ALL` or `LATEST`, by the `__tier__` constant a module states - # or by the rule where it states none, and `LATEST` runs on `latest` + # module in `ALL`, a curated list each stating `__tier__ = 'ALL'` (#1963), + # or else in `LATEST`, and `LATEST` runs on `latest` # alone, which is also the coverage leg, so coverage keeps its # selection. Every other leg -- `test-ktlo`'s too, as it runs these # steps -- passes each module latest_tier_paths() prints as an diff --git a/docs/source/contributing/workflows.rst b/docs/source/contributing/workflows.rst index 5323cd3d95..937da2705d 100644 --- a/docs/source/contributing/workflows.rst +++ b/docs/source/contributing/workflows.rst @@ -915,30 +915,57 @@ and fixture-dependent tiers, which decide what a module may read and so which jo collects it; a test tier decides on which of that job's legs it runs. ``module_tier()`` in :file:`tests/_tiers.py` decides, in one place, for every -``tests/**/test_*.py``: - -* **The marker.** A module may state its tier outright with a module-level - ``__tier__ = 'ALL'`` or ``__tier__ = 'LATEST'``, read by AST, under a ``#:`` - comment that gives the reason. It wins over the rule. It is a module constant - rather than a pytest marker, so ``unittest-ordering``'s plain ``unittest`` runs - read the same module. A value that is not one of the two string literals, or a - marker assigned twice or anywhere but at the module's top level, fails the tier - guard. -* **The rule.** Otherwise a module is ``LATEST`` when its run does not vary by - interpreter, by the same AST test as the project pins below: it reads the - repository's text, a workflow or the docs, or drives a ``util/`` script, and - neither imports ``pcapkit`` nor compares the Python version. Everything that - exercises ``pcapkit`` is ``ALL``. - -The marker says what the AST cannot see. Each real sweep -- a round trip over every -class or registry entry a table enumerates, every offset of a packet or every -capture -- is marked ``LATEST``, as the owner ruled on #1732 and #1958; the -hand-picked round trips, whatever their names, run on ``ALL``, and so do the #1202 -edge-case tables, whose cases are each written out by hand. A module that imports ``pcapkit`` only to read its text or -structure, or that drives a docs hook or a generator other legs already run, is -marked ``LATEST``; one that the rule would send to ``latest`` but that depends on -the interpreter, such as the tier guard itself, which :file:`tests/conftest.py` runs at -collection on every leg, is marked ``ALL``. +``tests/**/test_*.py``. ``ALL`` is a curated, opt-in list (#1958, #1963): short enough +to save time on every leg, the same on every leg so QA stays consistent, and enough +on its own to validate a change on each interpreter, while ``latest`` runs the whole +suite as the gate. + +* **The marker.** A module joins ``ALL`` with a module-level ``__tier__ = 'ALL'``, + read by AST, under a ``#: Test tier (#1958): `` comment. It is a module + constant rather than a pytest marker, so ``unittest-ordering``'s plain + ``unittest`` runs read the same module. A value that is not ``'ALL'`` or + ``'LATEST'``, or a marker assigned twice or anywhere but at the module's top + level, fails the tier guard. +* **The default.** A module that states no tier is ``LATEST``, so a new module runs + on ``latest`` until it says why every leg needs it. ``__tier__ = 'LATEST'`` is + optional, and records why a module someone might expect on every leg is not; a + module that compares ``sys.version_info``, ``sys.implementation`` or + ``sys.maxsize``, reads + ``sys.flags``, calls ``sysconfig.get_config_var()`` or probes :mod:`sys` with + ``hasattr``/``getattr`` has to state its tier either way, so a version gate never + lands on ``latest`` alone by omission. The check is syntactic and best-effort: an + alias, a flag imported from ``pcapkit.utilities.compat`` or a gate in a helper is + not seen, and such a module states its tier by hand. + +A module opts in when it exercises behaviour that can differ between interpreters +or versions: + +* compat shims and version or implementation gates; +* C-accelerated versus pure paths, and code sensitive to garbage collection, weak + references or finalisation; +* ``int``, ``bytes`` and ``struct`` edge cases, and ``sys.maxsize`` or recursion + limits; +* pickling and copying, metaclass and ABC machinery, and enum mechanics; +* stream and buffer semantics, threading and locking, and the warning and + exception hooks; +* a third-party engine whose installable releases vary by interpreter, such as + ``pypcap`` and ``pypcapfile``, which install below 3.12 only, and Scapy, which the + ``integration`` and ``pypcap-parity`` legs install on every interpreter and a + ``test`` leg off ``latest`` skips; +* the hand-picked round trips, whatever their names, and the #1202 edge-case + tables, whose cases are each written out by hand, as the owner ruled on #1958; + and +* the smoke slice: a representative module per protocol family and per package, + so each leg still drives the main parse and build paths. + +Everything else is ``LATEST``: the per-protocol field-by-field tests, whose +behaviour is interpreter-independent pure Python, each real sweep -- a round trip +over every class or registry entry a table enumerates, every offset of a packet or +every capture, as the owner ruled on #1732 and #1958 -- and the docs, project and +structure tests. The vendor extra is installed on ``latest`` alone and the crawlers +are pure Python, so the vendor modules are ``LATEST``, each saying so, but for the +file-restore and environment-variable tests, which stay on every leg. The tier guard itself is ``ALL``, since :file:`tests/conftest.py` +runs it at collection on every leg. Which legs run which tier: diff --git a/tests/_tiers.py b/tests/_tiers.py index 5bf864cd3d..4dca218957 100644 --- a/tests/_tiers.py +++ b/tests/_tiers.py @@ -47,10 +47,12 @@ for real, and the complete pytest selection the fixture-dependent tier needs in order to still exercise them. * :func:`module_tier` -- which *test tier* does this module run in (#1958): - ``ALL``, on every leg, or ``LATEST``, on ``latest`` alone? A ``__tier__`` - constant in the module (:func:`declared_tier`) answers first, and - :func:`rule_tier` otherwise. :func:`latest_tier_paths` lists the ``LATEST`` - modules, which every leg but ``latest`` leaves out. + ``ALL``, on every leg, or ``LATEST``, on ``latest`` alone? ``ALL`` is opt-in: + a module is in it only when it says so with ``__tier__ = 'ALL'`` + (:func:`declared_tier`), and :data:`DEFAULT_TIER`, ``LATEST``, otherwise. + :func:`all_tier_paths` and :func:`latest_tier_paths` list the two tiers; every + leg but ``latest`` leaves the second out. See "The curated ``ALL`` tier" below + for which modules opt in. * :func:`is_project_pin` and :func:`project_pin_paths` -- which :file:`tests/project` modules give one answer on every interpreter, and so run once per push, in the ``project-tests`` job, rather than on every leg (#1954)? @@ -114,6 +116,47 @@ capture reads go through :func:`~tests._support.sample_path`, and that is the door with the lock on it. +The curated ``ALL`` tier +------------------------ + +``ALL`` is a tailored list rather than whatever a rule guesses (#1958, #1963): it +is what every leg off ``latest`` runs, so it has to be short enough to save time, +the same on every leg so QA stays consistent, and enough on its own to validate a +change on each interpreter. ``latest`` runs the whole suite, and is the gate. + +A module opts in with ``__tier__ = 'ALL'`` when it exercises behaviour that can +differ between interpreters or versions: + +* compat shims and ``sys.version_info`` or ``sys.implementation`` gates; +* C-accelerated versus pure paths, and code sensitive to garbage collection, + weak references or finalisation; +* ``int``, ``bytes`` and :mod:`struct` edge cases, and ``sys.maxsize`` or + recursion limits; +* pickling and copying, metaclass and ABC machinery, and enum mechanics; +* stream and buffer semantics, threading and locking, and the warning and + exception hooks; +* a third-party engine whose installable releases vary by interpreter, which + ``latest`` alone could not cover; +* the hand-picked round trips, the ``*_edge_roundtrip_unit`` tables among them, + which the owner ruled run on ``ALL`` as part of QA; and +* the smoke slice: a representative module per protocol family and per + package, so each leg still drives the main parse and build paths. + +Everything else is ``LATEST`` by default: the per-protocol field-by-field tests, +whose behaviour is interpreter-independent pure Python, the real sweeps, and the +docs, project and structure tests. A module that branches on the interpreter +itself (:func:`branches_on_interpreter`, a syntactic, best-effort check) has to +state its tier either way, so a version gate never lands on ``latest`` alone by +omission; a ``LATEST`` marker is otherwise optional, and records why a module +someone might expect on every leg is not. + +Scapy is opted in like the other third-party engines: the ``integration`` and +``pypcap-parity`` legs install it on every interpreter, and a ``test`` leg off +``latest``, which does not, skips it. The vendor extra is installed on ``latest`` +alone, and the crawlers are pure Python over ``requests`` and ``bs4``, so the vendor +modules are ``LATEST``, each saying so, but for the file-restore and +environment-variable tests, which stay on every leg. + Nothing here imports :mod:`tests._support` -- the dependency runs the other way, and adding the reverse edge would make it a cycle. Nothing here imports :mod:`pcapkit`, :mod:`pytest` or any optional engine either, so the guard is @@ -141,7 +184,8 @@ 'ROOT', 'TESTS_ROOT', 'SAMPLE_ROOT', 'REGENERATE_SAMPLES_CMD', 'UNIT_TIER_SELECTION', 'FIXTURE_TIER_SUFFIXES', 'FIXTURE_TIER_DIRS', 'FIXTURE_TIER_SELECTOR', 'TIER_ALL', 'TIER_LATEST', 'TIERS', 'TIER_MARKER', 'LATEST_TIER_SELECTOR', 'FIXTURE_ALL_TIER_SELECTOR', - 'InvalidTierError', 'suite_modules', 'declared_tier', 'rule_tier', 'module_tier', 'latest_tier_paths', + 'InvalidTierError', 'DEFAULT_TIER', 'suite_modules', 'declared_tier', 'branches_on_interpreter', 'module_tier', + 'all_tier_paths', 'latest_tier_paths', 'PROJECT_PINS_DIR', 'PROJECT_PINS_SELECTOR', 'PROJECT_PIN_PACKAGE', 'LOADABLE_DIRS', 'varies_by_interpreter', 'is_project_pin', 'project_pin_paths', 'is_unit_tier', 'committed_captures', 'committed_capture_names', @@ -195,6 +239,9 @@ TIER_LATEST = 'LATEST' #: Every test tier, the values a :data:`TIER_MARKER` may take. TIERS = (TIER_ALL, TIER_LATEST) +#: The tier of a module that states none: ``ALL`` is opt-in (#1958, #1963), so a +#: new module runs on ``latest``, the gate, until it says why every leg needs it. +DEFAULT_TIER = TIER_LATEST #: The module-level constant that states a module's test tier outright, e.g. #: ``__tier__ = 'LATEST'``, read by AST in :func:`declared_tier`. A constant rather #: than a pytest marker, so plain :mod:`unittest` runs see the same module. @@ -282,7 +329,7 @@ class TierGuardWarning(UserWarning): class InvalidTierError(ValueError): """A test module's :data:`TIER_MARKER` cannot be read as one of :data:`TIERS`. - Raised rather than ignored: a marker that silently fell back to the rule would + Raised rather than ignored: a marker that silently fell back to the default would put the module on legs its author ruled it off, or the other way round. """ @@ -896,7 +943,7 @@ def declared_tier(path: 'pathlib.Path | str') -> 'Optional[str]': Read by AST, never by importing the module: one module-level statement, ``__tier__ = 'ALL'`` or ``__tier__ = 'LATEST'``, the value a string literal. - A module that does not parse states nothing here, and :func:`rule_tier` + A module that does not parse states nothing here, and :data:`DEFAULT_TIER` answers for it. Args: @@ -912,19 +959,56 @@ def declared_tier(path: 'pathlib.Path | str') -> 'Optional[str]': return _declared_tier(_test_module(path)) -def rule_tier(path: 'pathlib.Path | str') -> 'str': - """The test tier the rule gives the module at ``path`` (#1958). +#: Attributes of :mod:`sys` whose mere use asks which build is running, compared or not: +#: ``sys.flags.context_aware_warnings``, ``sys._is_gil_enabled()``. +_BUILD_FACTS = frozenset({'flags', '_is_gil_enabled'}) +#: Builtins that probe :mod:`sys` for an attribute, e.g. ``hasattr(sys, 'tracebacklimit')``. +_PROBES = frozenset({'hasattr', 'getattr'}) + + +def _probes_interpreter(node: 'ast.AST') -> 'bool': + """Whether ``node`` asks the running interpreter something outside a comparison. + + ``sys.flags`` or ``sys._is_gil_enabled``, a comparison with ``sys.maxsize``, a + ``sysconfig.get_config_var()`` call such as ``Py_GIL_DISABLED``'s, or a + ``hasattr``/``getattr`` probe of :mod:`sys`. + + """ + if isinstance(node, ast.Attribute): + return node.attr in _BUILD_FACTS and isinstance(node.value, ast.Name) and node.value.id == 'sys' + if isinstance(node, ast.Compare): + return any(isinstance(part, ast.Attribute) and part.attr == 'maxsize' and isinstance(part.value, ast.Name) + and part.value.id == 'sys' for operand in [node.left] + list(node.comparators) + for part in ast.walk(operand)) + if not isinstance(node, ast.Call): + return False + func = node.func + if isinstance(func, ast.Attribute) and func.attr == 'get_config_var': + return isinstance(func.value, ast.Name) and func.value.id == 'sysconfig' + if isinstance(func, ast.Name) and func.id == 'get_config_var': + return True + return (isinstance(func, ast.Name) and func.id in _PROBES and bool(node.args) + and isinstance(node.args[0], ast.Name) and node.args[0].id == 'sys') + + +def branches_on_interpreter(path: 'pathlib.Path | str') -> 'bool': + """Whether the test module at ``path`` itself asks which interpreter is running (#1958). + + By its own AST alone, unlike :func:`varies_by_interpreter`, which follows what + the module runs: a comparison of ``sys.version_info``, ``sys.implementation``, + ``platform.python_version()`` or the like; a use of ``sys.flags`` or + ``sys._is_gil_enabled``; a comparison with ``sys.maxsize``; a + ``sysconfig.get_config_var()`` call, such as + ``Py_GIL_DISABLED``'s; or a ``hasattr``/``getattr`` probe of :mod:`sys`, anywhere + in it. Such a module has to state its tier with :data:`TIER_MARKER`, so a + version gate never runs on ``latest`` alone just because nobody opted it in to + ``ALL``. A module that does not parse branches on nothing here. - :data:`TIER_LATEST` when its run does not :func:`vary by interpreter - ` -- it reads the repository's text, a workflow or the - docs, or drives a ``util/`` script, and imports no ``pcapkit`` -- since one run - on ``latest`` is then every leg's answer. :data:`TIER_ALL` for everything - else, which is everything that exercises ``pcapkit``. What the AST cannot - see -- that a module is a sweep over every case of a table or every capture, - that one which imports ``pcapkit`` still answers a question about the - repository, or that one which imports nothing still depends on the - interpreter -- is stated with :data:`TIER_MARKER` instead; see - :func:`module_tier`. + The detection is syntactic and best-effort, not proof that a module is + interpreter-independent: it does not see :mod:`sys` under an alias, a probe of + any other module, a flag imported from :mod:`pcapkit.utilities.compat` or + another helper, or a gate in code the module imports. A module that relies on + any of those states its tier by hand. Args: path: Path to a test module, absolute or relative to :data:`ROOT`. @@ -933,18 +1017,21 @@ def rule_tier(path: 'pathlib.Path | str') -> 'str': ValueError: ``path`` is not a test module under :file:`tests/`. """ - return TIER_ALL if varies_by_interpreter(_test_module(path)) else TIER_LATEST + tree = _parse_module(_test_module(path)) + return tree is not None and any(_compares_interpreter(node) or _probes_interpreter(node) + for node in ast.walk(tree)) def module_tier(path: 'pathlib.Path | str') -> 'str': """The test tier of the module at ``path``: :data:`TIER_ALL` or :data:`TIER_LATEST` (#1958). - Its :func:`declared_tier` where it states one, its :func:`rule_tier` - otherwise, so every test module has exactly one. ``ALL`` modules run on every - leg; ``LATEST`` ones on ``latest`` alone, which is also the coverage leg, the - ``gate`` job's interpreter, and every ``unittest-ordering`` cell's. Within a - tier, the unit and fixture-dependent tiers still decide which job collects a - module. + Its :func:`declared_tier` where it states one, :data:`DEFAULT_TIER` otherwise, + so every test module has exactly one and ``ALL`` is opt-in: see "The curated + ``ALL`` tier" in this module's docstring for which modules opt in. ``ALL`` + modules run on every leg; ``LATEST`` ones on ``latest`` alone, which is also + the coverage leg, the ``gate`` job's interpreter, and every + ``unittest-ordering`` cell's. Within a tier, the unit and fixture-dependent + tiers still decide which job collects a module. Args: path: Path to a test module, absolute or relative to :data:`ROOT`. @@ -954,8 +1041,18 @@ def module_tier(path: 'pathlib.Path | str') -> 'str': InvalidTierError: Its marker is malformed; see :func:`declared_tier`. """ - candidate = _test_module(path) - return _declared_tier(candidate) or rule_tier(candidate) + return _declared_tier(_test_module(path)) or DEFAULT_TIER + + +@functools.lru_cache(maxsize=1) +def all_tier_paths() -> 'tuple[str, ...]': + """Every :data:`TIER_ALL` test module, relative to :data:`ROOT`, sorted. + + The curated tier every leg runs: the complement of :func:`latest_tier_paths` + within :func:`suite_modules`, and each of them marked ``__tier__ = 'ALL'``. + + """ + return tuple(path for path in suite_modules() if module_tier(path) == TIER_ALL) @functools.lru_cache(maxsize=1) diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index 166aa76468..526beedd36 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -48,6 +48,10 @@ from tests._support import ISOLATED_PREFIXES, isolate_modules, snapshot_modules +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + ROOT = pathlib.Path(__file__).resolve().parents[2] #: The :data:`sys.modules` region this file stands things in for, and therefore diff --git a/tests/const/test_const_apptype_lazy_1538_unit.py b/tests/const/test_const_apptype_lazy_1538_unit.py index 0615b6509e..2b40365caf 100644 --- a/tests/const/test_const_apptype_lazy_1538_unit.py +++ b/tests/const/test_const_apptype_lazy_1538_unit.py @@ -39,6 +39,9 @@ from types import ModuleType from typing import Any +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + ROOT = pathlib.Path(__file__).resolve().parents[2] #: The names ``dir(pcapkit.const.reg.apptype)`` listed on the eager package diff --git a/tests/const/test_const_enum_builtin_parity.py b/tests/const/test_const_enum_builtin_parity.py index 615d62fe5b..eb2cb244a9 100644 --- a/tests/const/test_const_enum_builtin_parity.py +++ b/tests/const/test_const_enum_builtin_parity.py @@ -66,6 +66,9 @@ if TYPE_CHECKING: from typing import Optional +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + #: An integer no wire field in the library is wide enough to carry. Shared with #: :mod:`tests.const.test_const_enum_get`, which uses the same value for the #: same reason. diff --git a/tests/const/test_const_enum_get.py b/tests/const/test_const_enum_get.py index 8113f5fba1..7d7a3194b7 100644 --- a/tests/const/test_const_enum_get.py +++ b/tests/const/test_const_enum_get.py @@ -73,6 +73,10 @@ from tests._support import ISOLATED_PREFIXES, purge_modules, restore_modules, snapshot_modules +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + #: An integer no wire field in the library is wide enough to carry, so every #: registry that bounds its own domain rejects it. Deliberately far outside the #: ``0..0xFF`` range the Mobility Header flag registries check, because a value diff --git a/tests/const/test_const_enum_lookup.py b/tests/const/test_const_enum_lookup.py index 510bc74984..47f868bb94 100644 --- a/tests/const/test_const_enum_lookup.py +++ b/tests/const/test_const_enum_lookup.py @@ -72,6 +72,10 @@ from tests._support import (ISOLATED_PREFIXES, purge_modules, reimport_once_per_class, restore_modules, snapshot_modules) +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + #: Fully qualified names of the :class:`~aenum.IntEnum` classes under #: :mod:`pcapkit.const` for which rejecting ``0`` is *correct*, because their #: registries simply do not define ``0`` (registries reserving ``0`` and diff --git a/tests/const/test_const_register_loud_in_quiet_block_1916_unit.py b/tests/const/test_const_register_loud_in_quiet_block_1916_unit.py index c272d97039..6818970993 100644 --- a/tests/const/test_const_register_loud_in_quiet_block_1916_unit.py +++ b/tests/const/test_const_register_loud_in_quiet_block_1916_unit.py @@ -30,6 +30,10 @@ if TYPE_CHECKING: from typing import Callable +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + class RegisterLoudInQuietBlock1916Tests(unittest.TestCase): """``register`` on a generated registry logs its rejection inside a quiet block.""" diff --git a/tests/corekit/test_decorators.py b/tests/corekit/test_decorators.py index f4819e19b2..1fb7552649 100644 --- a/tests/corekit/test_decorators.py +++ b/tests/corekit/test_decorators.py @@ -7,6 +7,10 @@ from tests._support import (bootstrap_core_modules, install_fake_payload_protocols, isolate_modules) +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + class DecoratorTests(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/corekit/test_enum_get_exception_provenance_923_unit.py b/tests/corekit/test_enum_get_exception_provenance_923_unit.py index 4e0d80db6d..06875ce259 100644 --- a/tests/corekit/test_enum_get_exception_provenance_923_unit.py +++ b/tests/corekit/test_enum_get_exception_provenance_923_unit.py @@ -73,6 +73,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + __all__ = [ 'EnumKeyErrorClassTests', 'BaseGetProvenanceTests', 'BaseGetQuietnessTests', 'MethodStillMintsTests', 'TransportProtocolCaseFoldingTests', diff --git a/tests/corekit/test_enum_get_quiet_fallback_1845_unit.py b/tests/corekit/test_enum_get_quiet_fallback_1845_unit.py index 67e15b789c..be684a6baf 100644 --- a/tests/corekit/test_enum_get_quiet_fallback_1845_unit.py +++ b/tests/corekit/test_enum_get_quiet_fallback_1845_unit.py @@ -47,6 +47,10 @@ from tests.utilities._harness import Recorder +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + __all__ = ['QuietFallbackTests', 'QuietRaiseTests', 'LoudPathTests', 'MechanismTests'] #: A value :class:`ToSPrecedence`'s ``_validate_value`` refuses: the field is 3 bits. diff --git a/tests/corekit/test_enum_lookup_base_unit.py b/tests/corekit/test_enum_lookup_base_unit.py index 6738556682..f9693f428c 100644 --- a/tests/corekit/test_enum_lookup_base_unit.py +++ b/tests/corekit/test_enum_lookup_base_unit.py @@ -64,6 +64,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + #: The mutating half, which must stay on :class:`~pcapkit.corekit.enum.EnumRegistry`. MUTATING = ('register', 'register_alias', 'register_aliases', '_extend', '_unregistered_member') diff --git a/tests/corekit/test_enum_lookup_reparent_877_unit.py b/tests/corekit/test_enum_lookup_reparent_877_unit.py index 34f2057bf0..86d69dbc6e 100644 --- a/tests/corekit/test_enum_lookup_reparent_877_unit.py +++ b/tests/corekit/test_enum_lookup_reparent_877_unit.py @@ -60,6 +60,10 @@ from pcapkit.corekit.enum import EnumLookup +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + __all__ = [ 'ReparentedBasesTests', 'HttpV2FlagsHierarchyTests', 'PureReparentGetTests', 'TransportProtocolGetTests', 'CriticalityGetTests', 'NoMintingTests', diff --git a/tests/corekit/test_enum_lookup_reparent_930_unit.py b/tests/corekit/test_enum_lookup_reparent_930_unit.py index 4c54d535da..6879729fdc 100644 --- a/tests/corekit/test_enum_lookup_reparent_930_unit.py +++ b/tests/corekit/test_enum_lookup_reparent_930_unit.py @@ -148,6 +148,10 @@ from pcapkit.utilities.logging import logger from tests.utilities._harness import capture +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + __all__ = [ 'ReparentedBasesTests', 'GetByNameAndValueTests', 'FailedLookupTests', 'InheritedQuietnessTests', 'NoMintingTests', 'AllSevenInheritTheBareClassmethodTests', diff --git a/tests/corekit/test_fields_byteorder_1519_unit.py b/tests/corekit/test_fields_byteorder_1519_unit.py index 93748be3ef..c163ecf4e5 100644 --- a/tests/corekit/test_fields_byteorder_1519_unit.py +++ b/tests/corekit/test_fields_byteorder_1519_unit.py @@ -27,6 +27,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + SHARED = 'pcapkit.corekit.fields.byteorder' #: Where the helper lived before #1711; removed, not re-exported. diff --git a/tests/corekit/test_fields_copy_dispatch_unit.py b/tests/corekit/test_fields_copy_dispatch_unit.py index 658941c58d..364d791f47 100644 --- a/tests/corekit/test_fields_copy_dispatch_unit.py +++ b/tests/corekit/test_fields_copy_dispatch_unit.py @@ -41,6 +41,10 @@ from pcapkit.corekit.fields.field import FieldBase +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + class FieldCallUsesCopyDunderDirectlyTests(unittest.TestCase): """Every ``__call__`` in :mod:`pcapkit.corekit.fields` calls ``self.__copy__()``. diff --git a/tests/corekit/test_fields_field.py b/tests/corekit/test_fields_field.py index 4258370c92..4b7db1c861 100644 --- a/tests/corekit/test_fields_field.py +++ b/tests/corekit/test_fields_field.py @@ -7,6 +7,10 @@ from tests._support import reimport_once_per_class, time_limit +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + class FieldBaseUnpackBoundsTests(unittest.TestCase): """Bounds checking in :meth:`FieldBase.unpack `. diff --git a/tests/corekit/test_fields_misc.py b/tests/corekit/test_fields_misc.py index 973e9e6f93..2cbd94fa86 100644 --- a/tests/corekit/test_fields_misc.py +++ b/tests/corekit/test_fields_misc.py @@ -5,6 +5,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + #: Every address-typed branch of every address-typed #: :class:`~pcapkit.corekit.fields.misc.SwitchField` in the schema tree, as #: ``(module, class, attribute, label, packet)``. The ``packet`` is a selector diff --git a/tests/corekit/test_fields_numbers.py b/tests/corekit/test_fields_numbers.py index 185af50685..cfbec8a615 100644 --- a/tests/corekit/test_fields_numbers.py +++ b/tests/corekit/test_fields_numbers.py @@ -6,6 +6,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/corekit/test_fields_numbers_bit_length_1488_unit.py b/tests/corekit/test_fields_numbers_bit_length_1488_unit.py index af26cc204b..b64d1d1a6f 100644 --- a/tests/corekit/test_fields_numbers_bit_length_1488_unit.py +++ b/tests/corekit/test_fields_numbers_bit_length_1488_unit.py @@ -20,6 +20,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: Octets of a two-octet field, as hex. WIRES = ('0000', '0001', '0fff', 'ffff', 'f000', '8000', '0800', 'f7ff', 'a5a5') diff --git a/tests/corekit/test_fields_numbers_callable_length.py b/tests/corekit/test_fields_numbers_callable_length.py index f974622f82..11358a16d4 100644 --- a/tests/corekit/test_fields_numbers_callable_length.py +++ b/tests/corekit/test_fields_numbers_callable_length.py @@ -24,6 +24,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/corekit/test_fields_numbers_port_enum_field_peek_1774_unit.py b/tests/corekit/test_fields_numbers_port_enum_field_peek_1774_unit.py index 61459a253a..1d70ff5bdf 100644 --- a/tests/corekit/test_fields_numbers_port_enum_field_peek_1774_unit.py +++ b/tests/corekit/test_fields_numbers_port_enum_field_peek_1774_unit.py @@ -20,6 +20,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + #: An unassigned port on all four transports, in none of ``_missing_``'s spans. PORT = 50000 diff --git a/tests/corekit/test_fields_numbers_port_option_no_mint_unit.py b/tests/corekit/test_fields_numbers_port_option_no_mint_unit.py index dbdff0dd78..1ed8cf164f 100644 --- a/tests/corekit/test_fields_numbers_port_option_no_mint_unit.py +++ b/tests/corekit/test_fields_numbers_port_option_no_mint_unit.py @@ -52,6 +52,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + class IntCode(aenum.IntEnum): """An :class:`int`-backed registry stand-in. diff --git a/tests/corekit/test_fields_numbers_signed_decode_1274_unit.py b/tests/corekit/test_fields_numbers_signed_decode_1274_unit.py index 2add91d246..bb79621f08 100644 --- a/tests/corekit/test_fields_numbers_signed_decode_1274_unit.py +++ b/tests/corekit/test_fields_numbers_signed_decode_1274_unit.py @@ -18,6 +18,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + class TestSignedNumberFieldDecode(unittest.TestCase): """Pin the decoded value and the rebuilt octets of signed fields.""" diff --git a/tests/corekit/test_fields_numbers_width_repair.py b/tests/corekit/test_fields_numbers_width_repair.py index e363a6dd40..0982717132 100644 --- a/tests/corekit/test_fields_numbers_width_repair.py +++ b/tests/corekit/test_fields_numbers_width_repair.py @@ -42,6 +42,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/corekit/test_fields_strings.py b/tests/corekit/test_fields_strings.py index 45d467ca68..c7ad145b4c 100644 --- a/tests/corekit/test_fields_strings.py +++ b/tests/corekit/test_fields_strings.py @@ -4,6 +4,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + class BitFieldTests(unittest.TestCase): """Packing and parsing of :class:`~pcapkit.corekit.fields.strings.BitField`.""" diff --git a/tests/corekit/test_info_child_finalisation_1490_unit.py b/tests/corekit/test_info_child_finalisation_1490_unit.py index a4d07242d4..6ae1574d1b 100644 --- a/tests/corekit/test_info_child_finalisation_1490_unit.py +++ b/tests/corekit/test_info_child_finalisation_1490_unit.py @@ -23,6 +23,10 @@ from tests.corekit._roundtrip import skip_without_runtime +#: Test tier (#1958): depends on garbage collection, weak references or object +#: finalisation. +__tier__ = 'ALL' + ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) #: Parent and child, by ``module:qualname``; ``synthetic`` is declared in the script. diff --git a/tests/corekit/test_infoclass.py b/tests/corekit/test_infoclass.py index ae7807dd40..41e8a41b72 100644 --- a/tests/corekit/test_infoclass.py +++ b/tests/corekit/test_infoclass.py @@ -6,6 +6,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/corekit/test_io.py b/tests/corekit/test_io.py index 992a4c77f3..01af4a150b 100644 --- a/tests/corekit/test_io.py +++ b/tests/corekit/test_io.py @@ -9,6 +9,10 @@ from tests._support import bootstrap_core_modules, load_module, reimport_once_per_class +#: Test tier (#1958): exercises stream and buffer semantics, which differ by +#: interpreter. +__tier__ = 'ALL' + class SeekableReaderTests(unittest.TestCase): def _close_reader(self, reader) -> None: diff --git a/tests/corekit/test_io_peekable_stream_copy_1524_unit.py b/tests/corekit/test_io_peekable_stream_copy_1524_unit.py index f79e3ce41c..5a77d86d1b 100644 --- a/tests/corekit/test_io_peekable_stream_copy_1524_unit.py +++ b/tests/corekit/test_io_peekable_stream_copy_1524_unit.py @@ -23,6 +23,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + #: What :class:`_CopiesItself` returns from its own ``__deepcopy__``. _COPIED = 'the stream copied itself' diff --git a/tests/corekit/test_io_seekable_reader_short_reads_1586_unit.py b/tests/corekit/test_io_seekable_reader_short_reads_1586_unit.py index 5a09e97a1a..4b87bc6e29 100644 --- a/tests/corekit/test_io_seekable_reader_short_reads_1586_unit.py +++ b/tests/corekit/test_io_seekable_reader_short_reads_1586_unit.py @@ -24,6 +24,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises stream and buffer semantics, which differ by +#: interpreter. +__tier__ = 'ALL' + #: Octets the stream holds: more than a few buffers' worth. DATA = bytes(range(256)) * 4 diff --git a/tests/corekit/test_io_stream_proxies_1516_unit.py b/tests/corekit/test_io_stream_proxies_1516_unit.py index 73e622d941..ca8b904f38 100644 --- a/tests/corekit/test_io_stream_proxies_1516_unit.py +++ b/tests/corekit/test_io_stream_proxies_1516_unit.py @@ -22,6 +22,10 @@ from tests._support import reimport_once_per_class, sample_path +#: Test tier (#1958): depends on garbage collection, weak references or object +#: finalisation. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/corekit/test_ipv4interface_mask_unit.py b/tests/corekit/test_ipv4interface_mask_unit.py index 75ff9800dd..d663cd7ef1 100644 --- a/tests/corekit/test_ipv4interface_mask_unit.py +++ b/tests/corekit/test_ipv4interface_mask_unit.py @@ -26,6 +26,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + #: Address ``10.0.0.1`` followed by four mask octets. MASKS = { 'hostmask /24': '0a000001' '000000ff', diff --git a/tests/corekit/test_module.py b/tests/corekit/test_module.py index 6e03b691f3..57cafd499a 100644 --- a/tests/corekit/test_module.py +++ b/tests/corekit/test_module.py @@ -10,6 +10,10 @@ from tests._support import load_module, reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + class ModuleDescriptorTests(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/corekit/test_multidict.py b/tests/corekit/test_multidict.py index 79fb37da84..5a7cd0b88e 100644 --- a/tests/corekit/test_multidict.py +++ b/tests/corekit/test_multidict.py @@ -5,6 +5,10 @@ from tests._support import bootstrap_core_modules, reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + class MultiDictTests(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/corekit/test_multiinfo_1484_unit.py b/tests/corekit/test_multiinfo_1484_unit.py index ec8ff0ca01..736f775677 100644 --- a/tests/corekit/test_multiinfo_1484_unit.py +++ b/tests/corekit/test_multiinfo_1484_unit.py @@ -31,6 +31,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + PAIRS = [('a', 1), ('b', 2), ('a', 3)] diff --git a/tests/corekit/test_ordered_multidict_copy_unit.py b/tests/corekit/test_ordered_multidict_copy_unit.py index b411fd1a15..cfbf68970b 100644 --- a/tests/corekit/test_ordered_multidict_copy_unit.py +++ b/tests/corekit/test_ordered_multidict_copy_unit.py @@ -19,6 +19,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + #: Interleaved keys, so grouping by key changes the order. ITEMS = [('a', 1), ('b', 2), ('a', 3)] diff --git a/tests/corekit/test_sentinel_py_abc_1960_unit.py b/tests/corekit/test_sentinel_py_abc_1960_unit.py index 2732e2847f..5a10088226 100644 --- a/tests/corekit/test_sentinel_py_abc_1960_unit.py +++ b/tests/corekit/test_sentinel_py_abc_1960_unit.py @@ -33,6 +33,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + #: Repository root, put first on the child interpreter's path so that it #: imports this tree rather than whatever :mod:`pcapkit` is installed. REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/corekit/test_sentinel_singleton_1842_unit.py b/tests/corekit/test_sentinel_singleton_1842_unit.py index fd7ceecc32..3103ec22a7 100644 --- a/tests/corekit/test_sentinel_singleton_1842_unit.py +++ b/tests/corekit/test_sentinel_singleton_1842_unit.py @@ -42,6 +42,10 @@ if TYPE_CHECKING: from typing import Any, Callable +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + #: Where every sentinel is defined. CANONICAL_MODULE = 'pcapkit.corekit.sentinels' diff --git a/tests/corekit/test_sentinels_housing_unit.py b/tests/corekit/test_sentinels_housing_unit.py index 1a96d6f7bd..9e605b0ada 100644 --- a/tests/corekit/test_sentinels_housing_unit.py +++ b/tests/corekit/test_sentinels_housing_unit.py @@ -71,6 +71,10 @@ NoDefaultType, NoValueType, NullType) from tests._support import purge_modules +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + #: Every sentinel, as ``(instance name, consumer module, instance, type)``. The #: consumer module is the one that compares against the object, and so still #: imports it from :mod:`pcapkit.corekit.sentinels` -- without re-exporting it. diff --git a/tests/corekit/test_stringfield_lossless_unit.py b/tests/corekit/test_stringfield_lossless_unit.py index 188ea64300..46705b5ca9 100644 --- a/tests/corekit/test_stringfield_lossless_unit.py +++ b/tests/corekit/test_stringfield_lossless_unit.py @@ -31,6 +31,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: Charsets whose codecs reject a ``'surrogateescape'`` error handler, so that a #: fallback through one would raise where ``'replace'`` does not. NO_SURROGATE_CHARSETS = ('utf-16', 'utf-16-le', 'utf-16-be', 'utf-32', 'utf-7') diff --git a/tests/dumpkit/test_common_unit.py b/tests/dumpkit/test_common_unit.py index c2d19b6624..aeebb79064 100644 --- a/tests/dumpkit/test_common_unit.py +++ b/tests/dumpkit/test_common_unit.py @@ -16,6 +16,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/dumpkit/test_fallback_cycle_unit.py b/tests/dumpkit/test_fallback_cycle_unit.py index 3ec2a7790d..eb966d647c 100644 --- a/tests/dumpkit/test_fallback_cycle_unit.py +++ b/tests/dumpkit/test_fallback_cycle_unit.py @@ -31,6 +31,10 @@ except ImportError: # pragma: no cover - not POSIX resource = None # type: ignore[assignment] +#: Test tier (#1958): gates on GraalPy's sys.implementation, so it has to run on that +#: leg. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/dumpkit/test_pcap_trace_truncated_record_1541_unit.py b/tests/dumpkit/test_pcap_trace_truncated_record_1541_unit.py index 8caf566405..bc009b7b53 100644 --- a/tests/dumpkit/test_pcap_trace_truncated_record_1541_unit.py +++ b/tests/dumpkit/test_pcap_trace_truncated_record_1541_unit.py @@ -29,6 +29,10 @@ from tests._support import close_extractor, reimport_once_per_class from tests.foundation._roundtrip import ethernet, ipv4, pcap, pcapng, tcp +#: Test tier (#1958): a record whose claimed length runs towards the interpreter's size +#: limit, which differs by implementation (sys.maxsize, #1963). +__tier__ = 'ALL' + HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) diff --git a/tests/dumpkit/test_plist_date_unit.py b/tests/dumpkit/test_plist_date_unit.py index 1cd2233450..7883054e1a 100644 --- a/tests/dumpkit/test_plist_date_unit.py +++ b/tests/dumpkit/test_plist_date_unit.py @@ -24,6 +24,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises stream and buffer semantics, which differ by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/engines/test_engine_base.py b/tests/foundation/engines/test_engine_base.py index 4310e4539c..78e798ce82 100644 --- a/tests/foundation/engines/test_engine_base.py +++ b/tests/foundation/engines/test_engine_base.py @@ -8,6 +8,9 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/engines/test_pcap_ct_engine.py b/tests/foundation/engines/test_pcap_ct_engine.py index f3effa2f2e..0572306a91 100644 --- a/tests/foundation/engines/test_pcap_ct_engine.py +++ b/tests/foundation/engines/test_pcap_ct_engine.py @@ -28,6 +28,10 @@ from tests._support import close_extractor, reimport_once_per_class, sample_path +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/engines/test_pypcap_engine.py b/tests/foundation/engines/test_pypcap_engine.py index a1bc946637..f1fbb9b146 100644 --- a/tests/foundation/engines/test_pypcap_engine.py +++ b/tests/foundation/engines/test_pypcap_engine.py @@ -20,6 +20,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/engines/test_pypcapfile_engine.py b/tests/foundation/engines/test_pypcapfile_engine.py index 1f36588e0d..941e131911 100644 --- a/tests/foundation/engines/test_pypcapfile_engine.py +++ b/tests/foundation/engines/test_pypcapfile_engine.py @@ -28,6 +28,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/engines/test_pyshark_engine.py b/tests/foundation/engines/test_pyshark_engine.py index f338e235c2..e15a5df368 100644 --- a/tests/foundation/engines/test_pyshark_engine.py +++ b/tests/foundation/engines/test_pyshark_engine.py @@ -48,6 +48,10 @@ import unittest from unittest import mock +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/engines/test_pyshark_non_packet_unit.py b/tests/foundation/engines/test_pyshark_non_packet_unit.py index ebdd21f0b4..70d006a650 100644 --- a/tests/foundation/engines/test_pyshark_non_packet_unit.py +++ b/tests/foundation/engines/test_pyshark_non_packet_unit.py @@ -28,6 +28,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/engines/test_scapy_engine.py b/tests/foundation/engines/test_scapy_engine.py index 38353fd812..4d06ba61a3 100644 --- a/tests/foundation/engines/test_scapy_engine.py +++ b/tests/foundation/engines/test_scapy_engine.py @@ -46,6 +46,11 @@ from tests._support import sample_path +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + #: Repository root, which the child interpreter needs on :data:`sys.path` to #: import the :mod:`pcapkit` under test rather than an installed copy. ROOT = pathlib.Path(__file__).resolve().parents[3] diff --git a/tests/foundation/engines/test_scapy_pcapng_sections_runtime.py b/tests/foundation/engines/test_scapy_pcapng_sections_runtime.py index 40ddc31c0b..b2ef2bd83c 100644 --- a/tests/foundation/engines/test_scapy_pcapng_sections_runtime.py +++ b/tests/foundation/engines/test_scapy_pcapng_sections_runtime.py @@ -30,6 +30,11 @@ from tests._support import close_extractor, reimport_once_per_class, sample_path +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) HAS_SCAPY = importlib.util.find_spec('scapy') is not None diff --git a/tests/foundation/engines/test_scapy_reader_hooks_unit.py b/tests/foundation/engines/test_scapy_reader_hooks_unit.py index f0eb2b46f5..440fc8091c 100644 --- a/tests/foundation/engines/test_scapy_reader_hooks_unit.py +++ b/tests/foundation/engines/test_scapy_reader_hooks_unit.py @@ -26,6 +26,11 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + HAS_SCAPY = importlib.util.find_spec('scapy') is not None HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) diff --git a/tests/foundation/engines/test_scapy_reader_interfaces_runtime.py b/tests/foundation/engines/test_scapy_reader_interfaces_runtime.py index ded33cc6f0..02dbad5b9b 100644 --- a/tests/foundation/engines/test_scapy_reader_interfaces_runtime.py +++ b/tests/foundation/engines/test_scapy_reader_interfaces_runtime.py @@ -31,6 +31,11 @@ from tests._support import close_extractor, reimport_once_per_class, sample_path +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) HAS_SCAPY = importlib.util.find_spec('scapy') is not None diff --git a/tests/foundation/engines/test_scapy_record_length_1717_runtime.py b/tests/foundation/engines/test_scapy_record_length_1717_runtime.py index 5a5cd4e3dc..7a4a608883 100644 --- a/tests/foundation/engines/test_scapy_record_length_1717_runtime.py +++ b/tests/foundation/engines/test_scapy_record_length_1717_runtime.py @@ -19,6 +19,11 @@ from tests._support import close_extractor, reimport_once_per_class, sample_path +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + HAS_SCAPY = importlib.util.find_spec('scapy') is not None HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) diff --git a/tests/foundation/engines/test_scapy_record_length_1717_unit.py b/tests/foundation/engines/test_scapy_record_length_1717_unit.py index 6df7769c95..4e9fcf3892 100644 --- a/tests/foundation/engines/test_scapy_record_length_1717_unit.py +++ b/tests/foundation/engines/test_scapy_record_length_1717_unit.py @@ -22,6 +22,11 @@ from tests._support import close_extractor, reimport_once_per_class +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + HAS_SCAPY = importlib.util.find_spec('scapy') is not None HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) diff --git a/tests/foundation/reassembly/test_ip.py b/tests/foundation/reassembly/test_ip.py index 7bd1155d84..d4de9dfe6a 100644 --- a/tests/foundation/reassembly/test_ip.py +++ b/tests/foundation/reassembly/test_ip.py @@ -6,6 +6,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/reassembly/test_ip_max_length_edge_1739_unit.py b/tests/foundation/reassembly/test_ip_max_length_edge_1739_unit.py index 6d7fd6b129..fcf0e6bbd5 100644 --- a/tests/foundation/reassembly/test_ip_max_length_edge_1739_unit.py +++ b/tests/foundation/reassembly/test_ip_max_length_edge_1739_unit.py @@ -19,6 +19,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: Datagram octets, with no zero octet in them, so a hole shows. DATA = (bytes(range(1, 256)) * 258)[:65536] diff --git a/tests/foundation/reassembly/test_reassembly_base.py b/tests/foundation/reassembly/test_reassembly_base.py index 5f96e9cd92..4d63c2197b 100644 --- a/tests/foundation/reassembly/test_reassembly_base.py +++ b/tests/foundation/reassembly/test_reassembly_base.py @@ -8,6 +8,9 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/reassembly/test_tcp.py b/tests/foundation/reassembly/test_tcp.py index c93889cbc8..3468253eff 100644 --- a/tests/foundation/reassembly/test_tcp.py +++ b/tests/foundation/reassembly/test_tcp.py @@ -7,6 +7,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/registry/test_protocols.py b/tests/foundation/registry/test_protocols.py index d0d91f6d52..b96a09744b 100644 --- a/tests/foundation/registry/test_protocols.py +++ b/tests/foundation/registry/test_protocols.py @@ -8,6 +8,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/test_extraction.py b/tests/foundation/test_extraction.py index 9e001284de..7da9238b73 100644 --- a/tests/foundation/test_extraction.py +++ b/tests/foundation/test_extraction.py @@ -15,6 +15,10 @@ from tests._support import reimport_once_per_class, sample_path +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) #: Whether the optional DPKT engine can be selected. It is an extra diff --git a/tests/foundation/test_extraction_no_eof.py b/tests/foundation/test_extraction_no_eof.py index 9df6a21600..56b83110d3 100644 --- a/tests/foundation/test_extraction_no_eof.py +++ b/tests/foundation/test_extraction_no_eof.py @@ -108,6 +108,10 @@ from tests._support import sample_path, scale_timeout, time_limit from tests._tiers import ROOT +#: Test tier (#1958): exercises stream and buffer semantics, which differ by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/test_extraction_non_seekable_stream_1586_unit.py b/tests/foundation/test_extraction_non_seekable_stream_1586_unit.py index 2581e81a80..41176f46c5 100644 --- a/tests/foundation/test_extraction_non_seekable_stream_1586_unit.py +++ b/tests/foundation/test_extraction_non_seekable_stream_1586_unit.py @@ -45,6 +45,10 @@ from tests._support import reimport_once_per_class, scale_timeout from tests._tiers import ROOT +#: Test tier (#1958): exercises stream and buffer semantics, which differ by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) HAS_DPKT = importlib.util.find_spec('dpkt') is not None diff --git a/tests/foundation/test_extraction_ownership.py b/tests/foundation/test_extraction_ownership.py index 9a0d0ca77d..e827559e88 100644 --- a/tests/foundation/test_extraction_ownership.py +++ b/tests/foundation/test_extraction_ownership.py @@ -56,6 +56,10 @@ from tests._support import sample_path +#: Test tier (#1958): depends on garbage collection, weak references or object +#: finalisation. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/traceflow/test_tcp.py b/tests/foundation/traceflow/test_tcp.py index 1f9448222e..6d07b76d82 100644 --- a/tests/foundation/traceflow/test_tcp.py +++ b/tests/foundation/traceflow/test_tcp.py @@ -8,6 +8,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/foundation/traceflow/test_traceflow_base.py b/tests/foundation/traceflow/test_traceflow_base.py index cd114f78b0..05c03cea7b 100644 --- a/tests/foundation/traceflow/test_traceflow_base.py +++ b/tests/foundation/traceflow/test_traceflow_base.py @@ -9,6 +9,9 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/integration/test_engine_runtime.py b/tests/integration/test_engine_runtime.py index e836cddbaa..e0ae06bee1 100644 --- a/tests/integration/test_engine_runtime.py +++ b/tests/integration/test_engine_runtime.py @@ -8,6 +8,10 @@ from tests._support import close_extractor, reimport_once_per_class, sample_path +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) HAS_DPKT = importlib.util.find_spec('dpkt') is not None HAS_SCAPY = importlib.util.find_spec('scapy') is not None diff --git a/tests/integration/test_frame_iteration.py b/tests/integration/test_frame_iteration.py index 325ed764ac..41e06a8876 100644 --- a/tests/integration/test_frame_iteration.py +++ b/tests/integration/test_frame_iteration.py @@ -24,6 +24,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + #: Frames of :file:`http6.cap` that carry an HTTP header block: the request and #: the response of each of the two connections. HTTP_FRAMES = (4, 6, 19, 21) diff --git a/tests/interface/test_core.py b/tests/interface/test_core.py index 2c7f39bece..2c01891541 100644 --- a/tests/interface/test_core.py +++ b/tests/interface/test_core.py @@ -10,6 +10,10 @@ from tests._support import isolate_modules, load_module, reimport_once_per_class, sample_path +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) #: Whether the optional DPKT / Scapy engines can be selected. Both are extras diff --git a/tests/project/test_curated_all_tier_1958.py b/tests/project/test_curated_all_tier_1958.py new file mode 100644 index 0000000000..8fc5ede16b --- /dev/null +++ b/tests/project/test_curated_all_tier_1958.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +"""#1958, #1963: the ``ALL`` test tier is a curated, opt-in list. + +The owner's rulings, paraphrased: ``ALL`` is a tailored list of tests, to save +time, to keep QA consistent across legs, and to be enough to validate a change on +each of them; every implementation runs the same minimal ``ALL`` tier while +``latest`` runs the whole suite as the gate; and the hand-picked round trips run +on ``ALL`` as part of QA, with only the real sweeps on ``latest``. +:func:`tests._tiers.module_tier` is the one source of truth, and +:file:`tests/test_tier_guard.py` checks that every module lands in exactly one +tier. This module pins the curation itself: a module is in ``ALL`` only when it +says so, ``ALL`` stays a strict subset of the suite, and it keeps the ruling-B +round trips. + +""" + +from __future__ import annotations + +import pathlib +import tempfile +import unittest +import unittest.mock + +from tests import _tiers + +ROOT = pathlib.Path(__file__).resolve().parents[2] +DOC = ROOT / 'docs' / 'source' / 'contributing' / 'workflows.rst' +#: The #1202 edge-case tables, each case written out by hand (ruling B). +EDGE_TABLES = tuple(sorted(path.relative_to(ROOT).as_posix() + for path in (ROOT / 'tests' / 'protocols').glob('*/test_*_edge_roundtrip_unit.py'))) +#: The phrase every hand-picked round trip's marker reason carries. +HAND_PICKED = 'hand-picked' + + +def tier_of(source: 'str') -> 'str': + """The tier of a module holding ``source``, under a temporary :file:`tests/`.""" + with tempfile.TemporaryDirectory(prefix='pcapkit-curated-tier-') as temp: + root = pathlib.Path(temp).resolve() + path = root / 'test_probe.py' + path.write_text(source, encoding='utf-8') + with unittest.mock.patch.object(_tiers, 'TESTS_ROOT', root): + return _tiers.module_tier(path) + + +def reason(module: 'str') -> 'str': + """The ``#:`` comment above ``module``'s ``__tier__`` marker, joined into one line.""" + lines = (ROOT / module).read_text(encoding='utf-8').splitlines() + index = next(number for number, line in enumerate(lines) if line.startswith(_tiers.TIER_MARKER)) + comment = [] # type: list[str] + for line in reversed(lines[:index]): + if not line.startswith('#: '): + break + comment.insert(0, line[3:]) + return ' '.join(comment) + + +class TestAllIsOptIn(unittest.TestCase): + """Only ``__tier__ = 'ALL'`` puts a module in ``ALL``.""" + + def test_a_module_without_a_marker_is_latest(self) -> None: + for label, source in { + 'imports pcapkit': 'import pcapkit\n', + 'imports a pcapkit submodule': 'from pcapkit.protocols.link import Ethernet\n', + 'reads text': 'TEXT = "pcapkit"\n', + }.items(): + with self.subTest(case=label): + self.assertEqual(tier_of(source), _tiers.TIER_LATEST) + + def test_the_marker_opts_in(self) -> None: + self.assertEqual(tier_of("import pcapkit\n__tier__ = 'ALL'\n"), _tiers.TIER_ALL) + + def test_every_all_module_states_it(self) -> None: + for module in _tiers.suite_modules(): + if _tiers.module_tier(module) == _tiers.TIER_ALL: + with self.subTest(module=module): + self.assertEqual(_tiers.declared_tier(module), _tiers.TIER_ALL) + + +class TestAllIsAStrictSubset(unittest.TestCase): + """``ALL`` is a curated part of the suite, never the suite, and keeps the ruling-B round trips.""" + + def test_it_is_a_strict_subset_smaller_than_latest(self) -> None: + modules = set(_tiers.suite_modules()) + selected = {module for module in modules if _tiers.module_tier(module) == _tiers.TIER_ALL} + self.assertTrue(selected) + self.assertLess(selected, modules) + self.assertLess(len(selected), len(modules - selected)) + + def test_it_keeps_the_edge_case_tables(self) -> None: + self.assertTrue(EDGE_TABLES) + for module in EDGE_TABLES: + with self.subTest(module=module): + self.assertEqual(_tiers.module_tier(module), _tiers.TIER_ALL) + + def test_it_keeps_every_hand_picked_round_trip(self) -> None: + picked = [module for module in _tiers.suite_modules() + if _tiers.declared_tier(module) is not None and HAND_PICKED in reason(module)] + self.assertTrue(set(EDGE_TABLES) <= set(picked)) + for module in picked: + with self.subTest(module=module): + self.assertEqual(_tiers.module_tier(module), _tiers.TIER_ALL) + + def test_the_sweeps_stay_on_latest(self) -> None: + for module in ('tests/protocols/test_option_roundtrip_unit.py', + 'tests/protocols/test_short_read_roundtrip_unit.py', + 'tests/protocols/test_capture_roundtrip_runtime.py'): + with self.subTest(module=module): + self.assertEqual(_tiers.module_tier(module), _tiers.TIER_LATEST) + + +class TestTheDocsSayIt(unittest.TestCase): + + def test_the_workflows_page_states_the_criterion(self) -> None: + page = ' '.join(DOC.read_text(encoding='utf-8').split()) + section = page[page.index('Test Tiers ----------'):] + for phrase in ("``__tier__ = 'ALL'``", 'opt-in', '#1963', 'smoke slice', 'hand-picked round trips', + 'A module that states no tier is ``LATEST``'): + with self.subTest(phrase=phrase): + self.assertIn(phrase, section) + + def test_the_tier_module_states_the_criterion(self) -> None: + text = ' '.join(_tiers.__doc__.split()) + for phrase in ('The curated ``ALL`` tier', 'opt-in', 'smoke slice', '``*_edge_roundtrip_unit``'): + with self.subTest(phrase=phrase): + self.assertIn(phrase, text) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/project/test_public_api.py b/tests/project/test_public_api.py index f9021971b7..ebc388f15f 100644 --- a/tests/project/test_public_api.py +++ b/tests/project/test_public_api.py @@ -144,6 +144,10 @@ class exists at ``pcapkit/const/mh/handover_initiate_status.py`` and is used by import types import unittest +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + #: First path component under ``pcapkit.`` whose subtree the sweep skips. Empty: #: it held ``'vendor'`` until #1772, though ``requests`` and ``beautifulsoup4`` #: were in the ``test`` extra before the exclusion was written, and diff --git a/tests/project/test_sweeps_on_latest_only_1732.py b/tests/project/test_sweeps_on_latest_only_1732.py index d9be81d7b5..515005a102 100644 --- a/tests/project/test_sweeps_on_latest_only_1732.py +++ b/tests/project/test_sweeps_on_latest_only_1732.py @@ -317,10 +317,20 @@ def flags() -> 'list[str]': f'--junitxml={RUNNER_TEMP}/junit.xml'] def test_the_all_selection_leaves_out_the_latest_modules(self) -> None: - latest = [module for module in _tiers.latest_tier_paths() if not _tiers.is_unit_tier(module)] - self.assertTrue(latest) - modules = [{entry.split('::', 1)[0] for entry in entries} for entries in (self.selection, self.all)] - self.assertEqual(sorted(modules[0] - modules[1]), latest) + """What the ``ALL`` part drops is exactly the ``LATEST`` modules the whole selection reaches.""" + def reached(entries: 'list[str]') -> 'set[str]': + modules = set() # type: set[str] + for entry in entries: + module = entry.split('::', 1)[0] + if (_tiers.ROOT / module).is_dir(): + modules.update(path for path in _tiers.suite_modules() if path.startswith(module.rstrip('/') + '/')) + else: + modules.add(module) + return modules + everything, kept = reached(self.selection), reached(self.all) + dropped = everything - kept + self.assertTrue(dropped) + self.assertEqual(dropped, everything & set(_tiers.latest_tier_paths())) def test_only_latest_runs_them(self) -> None: """``latest`` passes the whole selection; every other leg its ``ALL`` part.""" diff --git a/tests/protocols/application/test_http_unit.py b/tests/protocols/application/test_http_unit.py index 370baf6325..fb39439ba2 100644 --- a/tests/protocols/application/test_http_unit.py +++ b/tests/protocols/application/test_http_unit.py @@ -8,6 +8,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/application/test_httpv2_negative_length_unit.py b/tests/protocols/application/test_httpv2_negative_length_unit.py index 55b1017af8..56fe4a6543 100644 --- a/tests/protocols/application/test_httpv2_negative_length_unit.py +++ b/tests/protocols/application/test_httpv2_negative_length_unit.py @@ -33,6 +33,10 @@ import unittest import warnings +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/internet/test_ipv4_unit.py b/tests/protocols/internet/test_ipv4_unit.py index a800ddac38..ea08cfe1a6 100644 --- a/tests/protocols/internet/test_ipv4_unit.py +++ b/tests/protocols/internet/test_ipv4_unit.py @@ -11,6 +11,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/internet/test_ipv6_calipso_make_validation_unit.py b/tests/protocols/internet/test_ipv6_calipso_make_validation_unit.py index f7b7ecfbb1..162dbde506 100644 --- a/tests/protocols/internet/test_ipv6_calipso_make_validation_unit.py +++ b/tests/protocols/internet/test_ipv6_calipso_make_validation_unit.py @@ -22,6 +22,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: The protocol classes sharing the option code, by module and class name. PROTOCOLS = ( ('pcapkit.protocols.internet.hopopt', 'HOPOPT'), diff --git a/tests/protocols/internet/test_ipv6_ext_chain_limit_1604_unit.py b/tests/protocols/internet/test_ipv6_ext_chain_limit_1604_unit.py index 0b900dfd00..7afb473cfa 100644 --- a/tests/protocols/internet/test_ipv6_ext_chain_limit_1604_unit.py +++ b/tests/protocols/internet/test_ipv6_ext_chain_limit_1604_unit.py @@ -27,6 +27,10 @@ from tests._support import reimport_once_per_class from tests.foundation import _roundtrip as wire +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: The TCP segment every chain ends in. SEGMENT = wire.tcp(b'payload', seq=1) diff --git a/tests/protocols/internet/test_ipv6_jumbogram_unit.py b/tests/protocols/internet/test_ipv6_jumbogram_unit.py index ec4e3c043a..31d512be8f 100644 --- a/tests/protocols/internet/test_ipv6_jumbogram_unit.py +++ b/tests/protocols/internet/test_ipv6_jumbogram_unit.py @@ -32,6 +32,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: The two IPv6 addresses, ``::1`` and ``::2``. ADDRS = bytes(15) + b'\x01' + bytes(15) + b'\x02' #: A TCP header of 20 octets, no options. diff --git a/tests/protocols/internet/test_ipv6_option_helpers_1519_unit.py b/tests/protocols/internet/test_ipv6_option_helpers_1519_unit.py index 674b2b001a..d6f0ac10cf 100644 --- a/tests/protocols/internet/test_ipv6_option_helpers_1519_unit.py +++ b/tests/protocols/internet/test_ipv6_option_helpers_1519_unit.py @@ -23,6 +23,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + SHARED = 'pcapkit.protocols.schema.internet.ipv6_option' #: Each header's schema module, and the prefix its errors carry. diff --git a/tests/protocols/internet/test_ipv6_unit.py b/tests/protocols/internet/test_ipv6_unit.py index f227120cbb..eaa96fca89 100644 --- a/tests/protocols/internet/test_ipv6_unit.py +++ b/tests/protocols/internet/test_ipv6_unit.py @@ -7,6 +7,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/internet/test_mh_timestamp_info_1886_unit.py b/tests/protocols/internet/test_mh_timestamp_info_1886_unit.py index 404ac34ab7..087244f36e 100644 --- a/tests/protocols/internet/test_mh_timestamp_info_1886_unit.py +++ b/tests/protocols/internet/test_mh_timestamp_info_1886_unit.py @@ -31,6 +31,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/link/test_link_unit.py b/tests/protocols/link/test_link_unit.py index 3a7bb32d97..7527a9c147 100644 --- a/tests/protocols/link/test_link_unit.py +++ b/tests/protocols/link/test_link_unit.py @@ -8,6 +8,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/misc/pcap/test_frame_incl_len_overrun_1541_unit.py b/tests/protocols/misc/pcap/test_frame_incl_len_overrun_1541_unit.py index 5e575a6a80..0a99ca0b40 100644 --- a/tests/protocols/misc/pcap/test_frame_incl_len_overrun_1541_unit.py +++ b/tests/protocols/misc/pcap/test_frame_incl_len_overrun_1541_unit.py @@ -21,6 +21,10 @@ from tests._support import close_extractor, reimport_once_per_class +#: Test tier (#1958): a record whose claimed length runs towards the interpreter's size +#: limit, which differs by implementation (sys.maxsize, #1963). +__tier__ = 'ALL' + #: Little-endian global header: v2.4, Ethernet. GLOBAL = bytes.fromhex('d4c3b2a1' '0200' '0400' '00000000' '00000000' 'ffff0000' '01000000') #: Sixty octets of packet data, none of them zero. diff --git a/tests/protocols/misc/pcap/test_header_frame_unit.py b/tests/protocols/misc/pcap/test_header_frame_unit.py index 5fa536862e..b733134b7f 100644 --- a/tests/protocols/misc/pcap/test_header_frame_unit.py +++ b/tests/protocols/misc/pcap/test_header_frame_unit.py @@ -11,6 +11,10 @@ from tests._support import reimport_once_per_class, sample_path +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/misc/test_pcapng_tzone_tsoffset_range_1340_unit.py b/tests/protocols/misc/test_pcapng_tzone_tsoffset_range_1340_unit.py index 684ffe5ebc..a52e869760 100644 --- a/tests/protocols/misc/test_pcapng_tzone_tsoffset_range_1340_unit.py +++ b/tests/protocols/misc/test_pcapng_tzone_tsoffset_range_1340_unit.py @@ -32,6 +32,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: ``datetime`` holds years 1 to 9999, so these are its first and last whole #: seconds since the UNIX epoch. DT_MIN = -62135596800 diff --git a/tests/protocols/misc/test_pcapng_unit.py b/tests/protocols/misc/test_pcapng_unit.py index a2fa28f7bc..eea90ad237 100644 --- a/tests/protocols/misc/test_pcapng_unit.py +++ b/tests/protocols/misc/test_pcapng_unit.py @@ -23,6 +23,10 @@ from tests._support import ROOT, reimport_once_per_class, sample_path, skip_if_stand_in +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/schema/test_enum_schema_registry_unit.py b/tests/protocols/schema/test_enum_schema_registry_unit.py index d5905229fb..e8e943fe30 100644 --- a/tests/protocols/schema/test_enum_schema_registry_unit.py +++ b/tests/protocols/schema/test_enum_schema_registry_unit.py @@ -23,6 +23,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py b/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py index bcb8ac5d6e..202356f3b6 100644 --- a/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py +++ b/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py @@ -8,6 +8,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/schema/test_schema_unit.py b/tests/protocols/schema/test_schema_unit.py index 8863c599e9..388338f0e1 100644 --- a/tests/protocols/schema/test_schema_unit.py +++ b/tests/protocols/schema/test_schema_unit.py @@ -10,6 +10,10 @@ from tests._support import reimport_once_per_class, time_limit +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/test_construction_keyword_check_unit.py b/tests/protocols/test_construction_keyword_check_unit.py index b4ef5e8b76..c85367a90c 100644 --- a/tests/protocols/test_construction_keyword_check_unit.py +++ b/tests/protocols/test_construction_keyword_check_unit.py @@ -82,6 +82,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/test_frame_layer_limit_1610_unit.py b/tests/protocols/test_frame_layer_limit_1610_unit.py index a1fea376a8..3263f57307 100644 --- a/tests/protocols/test_frame_layer_limit_1610_unit.py +++ b/tests/protocols/test_frame_layer_limit_1610_unit.py @@ -36,6 +36,10 @@ if TYPE_CHECKING: from typing import Optional +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: Little-endian PCAP global header: v2.4, snaplen 65535, Ethernet. GLOBAL = bytes.fromhex('d4c3b2a1' '0200' '0400' '00000000' '00000000' 'ffff0000' '01000000') diff --git a/tests/protocols/test_option_generator_deadline_scale_1428_unit.py b/tests/protocols/test_option_generator_deadline_scale_1428_unit.py index 454a626a15..8cf5e7235d 100644 --- a/tests/protocols/test_option_generator_deadline_scale_1428_unit.py +++ b/tests/protocols/test_option_generator_deadline_scale_1428_unit.py @@ -47,6 +47,9 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + #: The variable :mod:`tests._support` reads, spelled out so a generator reading a #: different name fails here rather than agreeing with itself. SCALE_ENV = 'PCAPKIT_TEST_TIMEOUT_SCALE' diff --git a/tests/protocols/test_protocol_base_unit.py b/tests/protocols/test_protocol_base_unit.py index e6126dee96..5db35cd3c1 100644 --- a/tests/protocols/test_protocol_base_unit.py +++ b/tests/protocols/test_protocol_base_unit.py @@ -10,6 +10,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/test_protocol_code_registration_unit.py b/tests/protocols/test_protocol_code_registration_unit.py index 1b3530a283..23bb89836c 100644 --- a/tests/protocols/test_protocol_code_registration_unit.py +++ b/tests/protocols/test_protocol_code_registration_unit.py @@ -35,6 +35,9 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/protocols/test_protocol_make_unit.py b/tests/protocols/test_protocol_make_unit.py index c60fb0fe33..9ac649e4e1 100644 --- a/tests/protocols/test_protocol_make_unit.py +++ b/tests/protocols/test_protocol_make_unit.py @@ -33,6 +33,9 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + def _attr(path: 'str') -> 'Any': module, _, name = path.rpartition('.') diff --git a/tests/protocols/test_to_dict_repeated_exthdr_1453_unit.py b/tests/protocols/test_to_dict_repeated_exthdr_1453_unit.py index d78b69b9c7..6a1b89d082 100644 --- a/tests/protocols/test_to_dict_repeated_exthdr_1453_unit.py +++ b/tests/protocols/test_to_dict_repeated_exthdr_1453_unit.py @@ -28,6 +28,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + HOP, DST, ROUTE, UDP = 0, 60, 43, 17 MAC = bytes.fromhex('0123456789ab' 'fedcba987654') SRC6 = bytes.fromhex('20010db8' + '00' * 11 + '01') diff --git a/tests/protocols/test_trial_parse_warnings_1580_unit.py b/tests/protocols/test_trial_parse_warnings_1580_unit.py index 61efc8e093..4b95a67e7c 100644 --- a/tests/protocols/test_trial_parse_warnings_1580_unit.py +++ b/tests/protocols/test_trial_parse_warnings_1580_unit.py @@ -40,6 +40,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): exercises the warning machinery itself, which free-threaded builds +#: make context-aware. +__tier__ = 'ALL' + #: Snaplen of the capture's interface, and so the octets each packet keeps. SNAPLEN = 10 #: Original length of each packet, longer than the Ethernet header. diff --git a/tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py b/tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py index 75e6bf5530..d5c396367d 100644 --- a/tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py @@ -146,6 +146,10 @@ import unittest +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: Header fields shared by the constructed TCP segments here, matching #: :data:`examples.generators.options.TCP_BASE` and #: :data:`tests.protocols.transport.test_tcp_mptcp_subtype_unit.TCP_BASE`. diff --git a/tests/protocols/transport/test_tcp_udp_unit.py b/tests/protocols/transport/test_tcp_udp_unit.py index a32ab1a089..b53851ba67 100644 --- a/tests/protocols/transport/test_tcp_udp_unit.py +++ b/tests/protocols/transport/test_tcp_udp_unit.py @@ -14,6 +14,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/test_base_class_contract.py b/tests/test_base_class_contract.py index ef8d5cddb2..934fe03c37 100644 --- a/tests/test_base_class_contract.py +++ b/tests/test_base_class_contract.py @@ -113,6 +113,10 @@ if TYPE_CHECKING: from typing import Any, Iterator +#: Test tier (#1958): exercises metaclass, ABC or enum machinery, which differs by +#: interpreter. +__tier__ = 'ALL' + #: Every ``*Base`` name mapped to the public name it must never be aliased to. #: Keyed on the base rather than the public name because the base is what an #: import statement names; the alias is the thing being outlawed. diff --git a/tests/test_final_enforcement.py b/tests/test_final_enforcement.py index c59db390dc..ff48fb06fb 100644 --- a/tests/test_final_enforcement.py +++ b/tests/test_final_enforcement.py @@ -82,6 +82,9 @@ import unittest import warnings +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/test_periodic_gc_1964_unit.py b/tests/test_periodic_gc_1964_unit.py index 762437d9f7..e7235f2c9c 100644 --- a/tests/test_periodic_gc_1964_unit.py +++ b/tests/test_periodic_gc_1964_unit.py @@ -16,6 +16,10 @@ from tests import conftest +#: Test tier (#1958): depends on garbage collection, weak references or object +#: finalisation. +__tier__ = 'ALL' + class TestPeriodicCollection(unittest.TestCase): diff --git a/tests/test_support_helpers.py b/tests/test_support_helpers.py index cbea1f5f21..a58f9a6cfb 100644 --- a/tests/test_support_helpers.py +++ b/tests/test_support_helpers.py @@ -81,6 +81,9 @@ if TYPE_CHECKING: from typing import Iterator +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + #: Seconds a :class:`TimeLimitTests` body spins waiting for the deadline before it #: gives up and fails by name: ten times the one-second deadline those tests arm, #: so a deadline that fires at all, its repeat included, lands well inside it. diff --git a/tests/test_tier_guard.py b/tests/test_tier_guard.py index 3501b08457..52511d1999 100644 --- a/tests/test_tier_guard.py +++ b/tests/test_tier_guard.py @@ -520,14 +520,20 @@ def test_every_module_resolves_to_exactly_one_tier(self) -> None: tiers.setdefault(tier, []).append(module) self.assertEqual(set(tiers), set(_tiers.TIERS)) self.assertEqual(list(_tiers.latest_tier_paths()), tiers[_tiers.TIER_LATEST]) + self.assertEqual(list(_tiers.all_tier_paths()), tiers[_tiers.TIER_ALL]) self.assertEqual(sorted(tiers[_tiers.TIER_ALL] + tiers[_tiers.TIER_LATEST]), list(modules)) + for module in tiers[_tiers.TIER_ALL]: + with self.subTest(module=module): + self.assertEqual(_tiers.declared_tier(module), _tiers.TIER_ALL) found = sorted(path.relative_to(_tiers.ROOT).as_posix() for path in _tiers.TESTS_ROOT.rglob('test_*.py')) self.assertEqual(list(modules), found) - def test_a_marker_wins_over_the_rule(self) -> None: + def test_all_is_opt_in(self) -> None: + """#1963: ``ALL`` is a curated list, so only a marker puts a module in it.""" cases = { - 'the rule, exercising pcapkit': ('import pcapkit\n', _tiers.TIER_ALL), - 'the rule, reading text': ('TEXT = "pcapkit"\n', _tiers.TIER_LATEST), + 'no marker, exercising pcapkit': ('import pcapkit\n', _tiers.TIER_LATEST), + 'no marker, reading text': ('TEXT = "pcapkit"\n', _tiers.TIER_LATEST), + 'opted in, exercising pcapkit': ("import pcapkit\n__tier__ = 'ALL'\n", _tiers.TIER_ALL), 'a sweep, marked': ("import pcapkit\n__tier__ = 'LATEST'\n", _tiers.TIER_LATEST), 'an interpreter-bound guard, marked': ("import ast\n__tier__ = 'ALL'\n", _tiers.TIER_ALL), 'annotated': ("import pcapkit\n__tier__: str = 'LATEST'\n", _tiers.TIER_LATEST), @@ -554,6 +560,42 @@ def test_an_invalid_marker_fails(self) -> None: self.assertIn('test_probe.py', str(caught.exception)) self.assertIn(_tiers.TIER_MARKER, str(caught.exception)) + def test_a_module_that_branches_on_the_interpreter_states_its_tier(self) -> None: + """A version gate is never ``LATEST`` by omission: such a module says which tier it is in. + + Detection is syntactic and best-effort: :mod:`sys` under an alias, a flag imported + from :mod:`pcapkit.utilities.compat`, or a gate in an imported helper is not seen. + + """ + cases = { + 'a version gate': ('import sys\nNEW = sys.version_info >= (3, 12)\n', True), + 'an implementation gate': ('import sys\nGRAAL = sys.implementation.name == "graalpy"\n', True), + 'a bare import': ('from sys import version_info\nOLD = version_info < (3, 9)\n', True), + 'pcapkit alone': ('import pcapkit\n', False), + 'a version only read': ('import sys\nprint(sys.version_info)\n', False), + 'a build flag': ('import sys\nAWARE = sys.flags.context_aware_warnings\n', True), + 'the GIL probe': ('import sys\nGIL = sys._is_gil_enabled()\n', True), + 'a config var': ('import sysconfig\nFT = sysconfig.get_config_var("Py_GIL_DISABLED")\n', True), + 'hasattr on sys': ('import sys\nSET = hasattr(sys, "tracebacklimit")\n', True), + 'getattr on sys': ('import sys\nLIMIT = getattr(sys, "tracebacklimit", None)\n', True), + 'getattr on another object': ('import os\nSEP = getattr(os, "sep")\n', False), + 'a size limit': ('import sys\nBIG = 1 << 40 > sys.maxsize\n', True), + 'a size limit only read': ('import sys\nprint(sys.maxsize)\n', False), + } + for label, (source, expected) in cases.items(): + with self.subTest(case=label), tempfile.TemporaryDirectory(prefix='pcapkit-test-tier-') as temp: + path = write_module(pathlib.Path(temp), 'test_probe.py', source) + with unittest.mock.patch.object(_tiers, 'TESTS_ROOT', pathlib.Path(temp).resolve()): + self.assertIs(_tiers.branches_on_interpreter(path), expected) + for module in _tiers.suite_modules(): + if _tiers.branches_on_interpreter(module): + with self.subTest(module=module): + self.assertIsNotNone(_tiers.declared_tier(module), + f'{module} branches on the interpreter; state its tier with ' + f"{_tiers.TIER_MARKER} = {_tiers.TIER_ALL!r} or {_tiers.TIER_LATEST!r} " + '(the check is syntactic and best-effort: aliases, probes of other ' + 'modules and flags imported from compat are not seen)') + def test_only_a_test_module_has_a_tier(self) -> None: for path in ('tests/_support.py', 'tests/_tiers.py', 'util/ci_timing.py', 'tests/project', 'tests/project/test_no_such_module.py'): @@ -562,7 +604,7 @@ def test_only_a_test_module_has_a_tier(self) -> None: _tiers.module_tier(path) def test_every_marker_states_a_reason(self) -> None: - """A ``__tier__`` the rule would also give says why it is there, as one that overrules it does.""" + """Every ``__tier__`` says why it is there, under a ``#: Test tier (#1958):`` comment.""" for module in _tiers.suite_modules(): if _tiers.declared_tier(module) is None: continue @@ -620,13 +662,17 @@ def test_a_directory_holding_a_latest_module_is_split(self) -> None: """pytest ignores no path it was handed, so the ``ALL`` selection names a mixed directory's modules.""" integration = f'tests/{next(iter(_tiers.FIXTURE_TIER_DIRS))}' members = [path for path in _tiers.suite_modules() if path.startswith(integration + '/')] - self.assertIn(integration, _tiers.fixture_tier_paths(_tiers.TIER_ALL)) - marked = members[0] _tiers.fixture_tier_paths.cache_clear() self.addCleanup(_tiers.fixture_tier_paths.cache_clear) real = _tiers.module_tier with unittest.mock.patch.object(_tiers, 'module_tier', - lambda path: _tiers.TIER_LATEST if path == marked else real(path)): + lambda path: _tiers.TIER_ALL if path in members else real(path)): + self.assertIn(integration, _tiers.fixture_tier_paths(_tiers.TIER_ALL)) + _tiers.fixture_tier_paths.cache_clear() + marked = members[0] + with unittest.mock.patch.object(_tiers, 'module_tier', + lambda path: _tiers.TIER_LATEST if path == marked + else _tiers.TIER_ALL if path in members else real(path)): selected = _tiers.fixture_tier_paths(_tiers.TIER_ALL) self.assertNotIn(integration, selected) self.assertNotIn(marked, selected) diff --git a/tests/toolkit/test_dpkt_timestamp_copy_1647_unit.py b/tests/toolkit/test_dpkt_timestamp_copy_1647_unit.py index 7de7a3f450..6f01fda56a 100644 --- a/tests/toolkit/test_dpkt_timestamp_copy_1647_unit.py +++ b/tests/toolkit/test_dpkt_timestamp_copy_1647_unit.py @@ -36,6 +36,10 @@ from pcapkit.toolkit.dpkt import RecordTimestamp +#: Test tier (#1958): exercises pickling and copying, whose protocols differ by +#: interpreter. +__tier__ = 'ALL' + ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) #: Rerun :func:`clones` over :func:`stamps` with :mod:`_decimal` unimportable, as diff --git a/tests/toolkit/test_dpkt_unit.py b/tests/toolkit/test_dpkt_unit.py index a6c1d335c1..63deb45bfd 100644 --- a/tests/toolkit/test_dpkt_unit.py +++ b/tests/toolkit/test_dpkt_unit.py @@ -12,6 +12,10 @@ from tests._support import close_extractor, reimport_once_per_class, sample_path +#: Test tier (#1958): the representative smoke slice for its package's main parse and +#: build paths. +__tier__ = 'ALL' + HAS_DPKT = importlib.util.find_spec('dpkt') is not None RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/toolkit/test_engine_pcap_trace_runtime.py b/tests/toolkit/test_engine_pcap_trace_runtime.py index 2f921f5f2e..a35a1816dd 100644 --- a/tests/toolkit/test_engine_pcap_trace_runtime.py +++ b/tests/toolkit/test_engine_pcap_trace_runtime.py @@ -38,6 +38,10 @@ if TYPE_CHECKING: from typing import Any +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) HAS_DPKT = importlib.util.find_spec('dpkt') is not None diff --git a/tests/toolkit/test_pcap_ct_unit.py b/tests/toolkit/test_pcap_ct_unit.py index cc8226e569..62e429da21 100644 --- a/tests/toolkit/test_pcap_ct_unit.py +++ b/tests/toolkit/test_pcap_ct_unit.py @@ -20,6 +20,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/toolkit/test_pypcap_unit.py b/tests/toolkit/test_pypcap_unit.py index 67b953e7ef..040d7c24bc 100644 --- a/tests/toolkit/test_pypcap_unit.py +++ b/tests/toolkit/test_pypcap_unit.py @@ -15,6 +15,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/toolkit/test_pypcapfile_unit.py b/tests/toolkit/test_pypcapfile_unit.py index ec0021c000..9565aaf411 100644 --- a/tests/toolkit/test_pypcapfile_unit.py +++ b/tests/toolkit/test_pypcapfile_unit.py @@ -24,6 +24,10 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/toolkit/test_pyshark_unit.py b/tests/toolkit/test_pyshark_unit.py index dc28ee05cb..28d29fb822 100644 --- a/tests/toolkit/test_pyshark_unit.py +++ b/tests/toolkit/test_pyshark_unit.py @@ -15,6 +15,10 @@ if TYPE_CHECKING: from typing import Any, Callable +#: Test tier (#1958): drives a third-party engine whose installable releases vary by +#: interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/toolkit/test_scapy_linktype_unit.py b/tests/toolkit/test_scapy_linktype_unit.py index b2ce69bba0..3b9602a3d1 100644 --- a/tests/toolkit/test_scapy_linktype_unit.py +++ b/tests/toolkit/test_scapy_linktype_unit.py @@ -17,6 +17,11 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + HAS_SCAPY = importlib.util.find_spec('scapy') is not None HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in ('tbtrim', 'aenum', 'chardet', 'dictdumper')) diff --git a/tests/toolkit/test_scapy_packet2dict_unit.py b/tests/toolkit/test_scapy_packet2dict_unit.py index e116f636f5..de02d8deb4 100644 --- a/tests/toolkit/test_scapy_packet2dict_unit.py +++ b/tests/toolkit/test_scapy_packet2dict_unit.py @@ -7,6 +7,11 @@ from tests._support import reimport_once_per_class +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + HAS_SCAPY = importlib.util.find_spec('scapy') is not None RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/toolkit/test_scapy_unit.py b/tests/toolkit/test_scapy_unit.py index 27b949e453..a81cd7410b 100644 --- a/tests/toolkit/test_scapy_unit.py +++ b/tests/toolkit/test_scapy_unit.py @@ -13,6 +13,11 @@ from tests._support import close_extractor, purge_modules, reimport_once_per_class +#: Test tier (#1958): drives Scapy, whose installable releases vary by interpreter; the +#: integration and pypcap-parity legs install it on every interpreter, and a test leg +#: off latest skips it. +__tier__ = 'ALL' + HAS_SCAPY = importlib.util.find_spec('scapy') is not None RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/utilities/test_compat.py b/tests/utilities/test_compat.py index 55e505b935..c6bae26df8 100644 --- a/tests/utilities/test_compat.py +++ b/tests/utilities/test_compat.py @@ -67,6 +67,9 @@ from tests._support import load_module, purge_modules, reimport_once_per_class from tests._tiers import ROOT +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + # NOTE: :mod:`aenum` is deliberately *not* imported at module scope, however # convenient that would be for the assertions below. A module-level import would # warm the cache issue #687 is about as a side effect of this file being diff --git a/tests/utilities/test_config_unit.py b/tests/utilities/test_config_unit.py index 7f47b7ad83..78d0b833ea 100644 --- a/tests/utilities/test_config_unit.py +++ b/tests/utilities/test_config_unit.py @@ -34,6 +34,10 @@ if TYPE_CHECKING: from typing import Any, Iterator +#: Test tier (#1958): exercises integer, bytes and struct edge cases, whose limits +#: differ by interpreter. +__tier__ = 'ALL' + #: Root of the :mod:`pcapkit` package under test. PACKAGE = pathlib.Path(__file__).resolve().parents[2] / 'pcapkit' diff --git a/tests/utilities/test_exceptions_excepthook.py b/tests/utilities/test_exceptions_excepthook.py index 0c1ee6c292..d6eb25b599 100644 --- a/tests/utilities/test_exceptions_excepthook.py +++ b/tests/utilities/test_exceptions_excepthook.py @@ -47,6 +47,10 @@ from tests._support import purge_modules from tests.utilities._harness import bootstrap, capture +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + #: Repository root -- three directories up from this file #: (``tests/utilities/test_exceptions_excepthook.py``). Passed to the child #: interpreter as ``PYTHONPATH`` explicitly, rather than relied on via the diff --git a/tests/utilities/test_exceptions_warnings.py b/tests/utilities/test_exceptions_warnings.py index 9bcc5af685..c87da7ba73 100644 --- a/tests/utilities/test_exceptions_warnings.py +++ b/tests/utilities/test_exceptions_warnings.py @@ -7,6 +7,10 @@ from tests._support import bootstrap_core_modules, reimport_once_per_class +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + class ExceptionsWarningsTests(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/utilities/test_logging.py b/tests/utilities/test_logging.py index e63be66845..9b2005fedd 100644 --- a/tests/utilities/test_logging.py +++ b/tests/utilities/test_logging.py @@ -9,6 +9,10 @@ from tests._support import load_module, reimport_once_per_class +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) diff --git a/tests/utilities/test_quiet_exceptions.py b/tests/utilities/test_quiet_exceptions.py index 4ed1a19490..f655aeec26 100644 --- a/tests/utilities/test_quiet_exceptions.py +++ b/tests/utilities/test_quiet_exceptions.py @@ -26,6 +26,10 @@ from tests._support import purge_modules from tests.utilities._harness import bootstrap, capture +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + def _unrelated_failure() -> 'int': """Format an unrelated exception and count the traceback's lines. diff --git a/tests/utilities/test_stacklevel.py b/tests/utilities/test_stacklevel.py index d7fca2a91e..8ded5a628b 100644 --- a/tests/utilities/test_stacklevel.py +++ b/tests/utilities/test_stacklevel.py @@ -58,6 +58,10 @@ _T = TypeVar('_T') +#: Test tier (#1958): exercises threading, locking or warning and exception hooks, which +#: differ by interpreter. +__tier__ = 'ALL' + #: The path fragment :func:`~pcapkit.utilities.exceptions.stacklevel` treats as #: "inside :mod:`pcapkit`". MARKER = f'{os.path.sep}pcapkit{os.path.sep}' diff --git a/tests/utilities/test_warning_emission.py b/tests/utilities/test_warning_emission.py index 1dc38c8b97..e6b7969c0d 100644 --- a/tests/utilities/test_warning_emission.py +++ b/tests/utilities/test_warning_emission.py @@ -27,6 +27,10 @@ from tests._support import purge_modules from tests.utilities._harness import bootstrap, capture +#: Test tier (#1958): exercises the warning machinery itself, which free-threaded builds +#: make context-aware. +__tier__ = 'ALL' + class WarningEmissionTests(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/utilities/test_warning_filters.py b/tests/utilities/test_warning_filters.py index f697e4c3e8..049d30d69a 100644 --- a/tests/utilities/test_warning_filters.py +++ b/tests/utilities/test_warning_filters.py @@ -29,6 +29,10 @@ from tests.utilities import _unrelated_warning from tests.utilities._harness import bootstrap, capture +#: Test tier (#1958): exercises the warning machinery itself, which free-threaded builds +#: make context-aware. +__tier__ = 'ALL' + #: Every warning category the module exports, by name. CATEGORY_NAMES = [ 'BaseWarning', diff --git a/tests/vendor/test_ftp_return_code_unit.py b/tests/vendor/test_ftp_return_code_unit.py index 6445018c7f..1669b95686 100644 --- a/tests/vendor/test_ftp_return_code_unit.py +++ b/tests/vendor/test_ftp_return_code_unit.py @@ -44,9 +44,12 @@ if TYPE_CHECKING: from typing import Any -#: Test tier (#1958): exercises pcapkit.vendor, which users run on any supported -#: interpreter. -__tier__ = 'ALL' +#: Test tier (#1958): the vendor extra is installed on latest alone, where the whole +#: suite runs, and the crawlers are pure Python over requests and bs4; the vendor +#: package's interpreter-sensitive parts, file restore and environment-variable parsing, +#: run on every leg in tests/vendor/test_vendor_snapshot_restore_unit.py and +#: tests/vendor/test_vendor_variables_1726_unit.py. +__tier__ = 'LATEST' #: Repository root, i.e. the grandparent of the directory holding this file. ROOT = pathlib.Path(__file__).resolve().parents[2] diff --git a/tests/vendor/test_re_sub_positional_flag_unit.py b/tests/vendor/test_re_sub_positional_flag_unit.py index 380ad26cb3..1866757c1c 100644 --- a/tests/vendor/test_re_sub_positional_flag_unit.py +++ b/tests/vendor/test_re_sub_positional_flag_unit.py @@ -86,6 +86,9 @@ import unittest import warnings +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + #: Repository root, i.e. the grandparent of the directory holding this file. ROOT = pathlib.Path(__file__).resolve().parents[2] diff --git a/tests/vendor/test_request_prompt_unit.py b/tests/vendor/test_request_prompt_unit.py index 039ba91ec9..98f4a54185 100644 --- a/tests/vendor/test_request_prompt_unit.py +++ b/tests/vendor/test_request_prompt_unit.py @@ -88,9 +88,12 @@ if TYPE_CHECKING: from typing import Any, Iterator -#: Test tier (#1958): exercises pcapkit.vendor, which users run on any supported -#: interpreter. -__tier__ = 'ALL' +#: Test tier (#1958): the vendor extra is installed on latest alone, where the whole +#: suite runs, and the crawlers are pure Python over requests and bs4; the vendor +#: package's interpreter-sensitive parts, file restore and environment-variable parsing, +#: run on every leg in tests/vendor/test_vendor_snapshot_restore_unit.py and +#: tests/vendor/test_vendor_variables_1726_unit.py. +__tier__ = 'LATEST' #: Repository root, i.e. the grandparent of the directory holding this file. ROOT = pathlib.Path(__file__).resolve().parents[2] diff --git a/tests/vendor/test_user_agent_unit.py b/tests/vendor/test_user_agent_unit.py index 9fa7286825..2cf613b881 100644 --- a/tests/vendor/test_user_agent_unit.py +++ b/tests/vendor/test_user_agent_unit.py @@ -50,9 +50,12 @@ if TYPE_CHECKING: from typing import Any, Iterator -#: Test tier (#1958): exercises pcapkit.vendor, which users run on any supported -#: interpreter. -__tier__ = 'ALL' +#: Test tier (#1958): the vendor extra is installed on latest alone, where the whole +#: suite runs, and the crawlers are pure Python over requests and bs4; the vendor +#: package's interpreter-sensitive parts, file restore and environment-variable parsing, +#: run on every leg in tests/vendor/test_vendor_snapshot_restore_unit.py and +#: tests/vendor/test_vendor_variables_1726_unit.py. +__tier__ = 'LATEST' #: Repository root, i.e. the grandparent of the directory holding this file. ROOT = pathlib.Path(__file__).resolve().parents[2] diff --git a/tests/vendor/test_vendor_dest_path_unit.py b/tests/vendor/test_vendor_dest_path_unit.py index 493e666da3..4e8cb0b7f5 100644 --- a/tests/vendor/test_vendor_dest_path_unit.py +++ b/tests/vendor/test_vendor_dest_path_unit.py @@ -123,9 +123,12 @@ if TYPE_CHECKING: from typing import Any -#: Test tier (#1958): exercises pcapkit.vendor, which users run on any supported -#: interpreter. -__tier__ = 'ALL' +#: Test tier (#1958): the vendor extra is installed on latest alone, where the whole +#: suite runs, and the crawlers are pure Python over requests and bs4; the vendor +#: package's interpreter-sensitive parts, file restore and environment-variable parsing, +#: run on every leg in tests/vendor/test_vendor_snapshot_restore_unit.py and +#: tests/vendor/test_vendor_variables_1726_unit.py. +__tier__ = 'LATEST' #: Repository root, i.e. the grandparent of the directory holding this file. ROOT = pathlib.Path(__file__).resolve().parents[2] diff --git a/tests/vendor/test_vendor_exit_code_unit.py b/tests/vendor/test_vendor_exit_code_unit.py index 50c596253e..ef0a15f245 100644 --- a/tests/vendor/test_vendor_exit_code_unit.py +++ b/tests/vendor/test_vendor_exit_code_unit.py @@ -63,9 +63,12 @@ import warnings from unittest import mock -#: Test tier (#1958): exercises pcapkit.vendor, which users run on any supported -#: interpreter. -__tier__ = 'ALL' +#: Test tier (#1958): the vendor extra is installed on latest alone, where the whole +#: suite runs, and the crawlers are pure Python over requests and bs4; the vendor +#: package's interpreter-sensitive parts, file restore and environment-variable parsing, +#: run on every leg in tests/vendor/test_vendor_snapshot_restore_unit.py and +#: tests/vendor/test_vendor_variables_1726_unit.py. +__tier__ = 'LATEST' #: Same gate :file:`tests/vendor/test_crawler_reachability_unit.py` and #: :file:`tests/vendor/test_vendor_dest_path_unit.py` use. Importing diff --git a/tests/vendor/test_vendor_missing_extra_1850_unit.py b/tests/vendor/test_vendor_missing_extra_1850_unit.py index 9afffc7ba6..1af475a412 100644 --- a/tests/vendor/test_vendor_missing_extra_1850_unit.py +++ b/tests/vendor/test_vendor_missing_extra_1850_unit.py @@ -53,6 +53,9 @@ if TYPE_CHECKING: from typing import Sequence +#: Test tier (#1958): branches on the running interpreter's version or implementation. +__tier__ = 'ALL' + #: The package under test, found from this file rather than by importing it, so #: that a child interpreter can be pointed at the same tree. PACKAGE = pathlib.Path(__file__).resolve().parents[2] / 'pcapkit' diff --git a/tests/vendor/test_vendor_ngap_unit.py b/tests/vendor/test_vendor_ngap_unit.py index fc01df9840..4e9b6ef14d 100644 --- a/tests/vendor/test_vendor_ngap_unit.py +++ b/tests/vendor/test_vendor_ngap_unit.py @@ -31,9 +31,12 @@ import unittest from unittest import mock -#: Test tier (#1958): exercises pcapkit.vendor, which users run on any supported -#: interpreter. -__tier__ = 'ALL' +#: Test tier (#1958): the vendor extra is installed on latest alone, where the whole +#: suite runs, and the crawlers are pure Python over requests and bs4; the vendor +#: package's interpreter-sensitive parts, file restore and environment-variable parsing, +#: run on every leg in tests/vendor/test_vendor_snapshot_restore_unit.py and +#: tests/vendor/test_vendor_variables_1726_unit.py. +__tier__ = 'LATEST' HAS_PYCRATE = importlib.util.find_spec('pycrate_asn1dir') is not None