diff --git a/tests/_support.py b/tests/_support.py index d2a5bd4be..df6288d4e 100644 --- a/tests/_support.py +++ b/tests/_support.py @@ -853,6 +853,74 @@ def purge_modules(prefixes: Iterable[str]) -> None: _reset_abc_caches() +#: Class attribute :func:`reimport_once_per_class` keeps a class's own import in. +_CLASS_IMPORT = '_pcapkit_class_import' + + +def reimport_once_per_class(test: 'unittest.TestCase', restore: bool = False) -> None: + """Give ``test``'s class one fresh :mod:`pcapkit` import, shared by its tests. + + GitHub issue #1065. Calling :func:`purge_modules` from ``setUp`` re-imports + the package before every test -- about 0.67s and 329 modules each, which was + some 85% of the suite's serial time. Call this from ``setUp`` instead. The + first test of the class purges, and the class keeps what that test imports. + Every later test of the class has that same import put back in + :data:`sys.modules`. So a class gets a private import, which its own tests + may mutate, and the import is dropped when the class finishes. Nothing is + imported here: a class that only loads modules by path, or that never + imports :mod:`pcapkit` at all, keeps an empty region, as it did before. + + A purge in ``setUpClass`` would not do this under :program:`pytest`. + :func:`tests.conftest.restore_module_table` swaps the region back to its + session-wide pin before every test, so such a class would run on the shared + import that every other class mutates as well. That is exactly the + cross-class leakage a per-test purge used to hide. + + Under plain :mod:`unittest`, the import is also left in place after the class + finishes, as a per-test purge left one. ``util/run_unittest_leg.py`` relies + on that to catch a module still holding an earlier import (GitHub issue + #981). + + Args: + test: The test being set up. + restore: Also put back, once the class finishes, whatever the region held + before its first test. This is the per-class form of a ``setUp`` that + snapshots, purges and restores around each test. + + """ + cls = type(test) + held = cls.__dict__.get(_CLASS_IMPORT) + if held is not None: + restore_modules(held, ISOLATED_PREFIXES) + else: + if restore: + cls.addClassCleanup(restore_modules, snapshot_modules(ISOLATED_PREFIXES), ISOLATED_PREFIXES) + purge_modules(['pcapkit']) + held = {} + setattr(cls, _CLASS_IMPORT, held) + cls.addClassCleanup(delattr, cls, _CLASS_IMPORT) + # Registered first, so it runs after the test's own cleanups. + test.addCleanup(_keep_lazy_imports, held) + + +def _keep_lazy_imports(held: 'dict[str, types.ModuleType]') -> None: + """Add to ``held`` the real :mod:`pcapkit` modules the test imported. + + This is how the class's import is built up. Without it, the conftest restore + drops a submodule imported during the test, and the next test of the class + imports it again as a second module object. The parent package still holds the first one as an attribute, and + :func:`unittest.mock.patch` resolves through that attribute before Python + 3.12 -- so a patch landed on a module the code under test no longer used. + Only modules loaded from a file are kept; a stand-in a test bound, and its + own cleanup has not removed, is not. + + """ + for name, module in snapshot_modules(ISOLATED_PREFIXES).items(): + spec = getattr(module, '__spec__', None) + if name not in held and spec is not None and spec.origin and getattr(module, '__name__', None) == name: + held[name] = module + + def _close_quietly(target: object) -> None: """Call ``target.close()``, swallowing any :exc:`Exception` it raises. diff --git a/tests/const/test_const_apptype_split_unit.py b/tests/const/test_const_apptype_split_unit.py index e198baf3a..83c920f19 100644 --- a/tests/const/test_const_apptype_split_unit.py +++ b/tests/const/test_const_apptype_split_unit.py @@ -32,6 +32,29 @@ __all__ = ['AppTypeSplitTests'] +#: Stride of :func:`_dunder_sample`: about 48 evenly spaced members of each +#: registry, plus the ones it always keeps. +_DUNDER_SAMPLE_SIZE = 48 + + +def _dunder_sample(registry: 'type') -> 'list': + """The members of ``registry`` the byte-identity sweep formats, in row order. + + GitHub issue #1062. Sweeping every member ran 37,173 subtests, and per-test + coverage contexts found only **4** distinct arc sets among them: ``repr`` + and ``str``, each with and without an ``(aliases: ...)`` suffix -- the + registry, the port and the service name never change which code runs. So + the sample keeps every member that carries an alias (the suffix branch), + the first and last row, and an evenly spaced stride through the rest, which + covers all four sets in every registry and is the same list on every run. + + """ + members = list(registry) + step = max(1, len(members) // _DUNDER_SAMPLE_SIZE) + keep = set(range(0, len(members), step)) | {len(members) - 1} + return [member for index, member in enumerate(members) + if index in keep or member.aliases] + class AppTypeSplitTests(unittest.TestCase): """The shape of the split registry.""" @@ -1185,13 +1208,16 @@ def test_the_new_dunders_are_byte_identical_to_the_percent_form(self) -> None: """GitHub issue #798: ``AppType``'s ``__new__``/``__repr__``/``__str__`` moved from ``%`` formatting to f-strings, and ``__new__``'s format sets every real member's underlying :class:`~aenum.StrEnum` value -- a far larger - blast radius than an error path, so this is checked member by member - rather than spot-checked. - - Swept over all 12,391 real members (TCP 6147, UDP 6143, SCTP 91, - DCCP 10, matching the population PR #783 measured), each - compared against what the pre-#798 ``%``-style formula would have - produced for that same member's own ``svc``/``port``/``proto``. This + blast radius than an error path. This was once checked member by + member; it is now a deterministic sample, because one f-string formats + every member and the sweep only ever reached four distinct code paths + (see :func:`_dunder_sample`). + + The population is still counted over all 12,391 real members (TCP 6147, + UDP 6143, SCTP 91, DCCP 10, matching the population PR #783 measured), + but only :func:`_dunder_sample` is formatted, each member compared + against what the pre-#798 ``%``-style formula would have produced for + that same member's own ``svc``/``port``/``proto``. This is an invariance check -- it is true either side of #798's fix by construction, since both formulas render the same text for the same inputs -- rather than a regression test that fails on stock ``main``. @@ -1224,12 +1250,14 @@ def test_the_new_dunders_are_byte_identical_to_the_percent_form(self) -> None: registries = {'TCP': TCP, 'UDP': UDP, 'SCTP': SCTP, 'DCCP': DCCP} expected_counts = {'TCP': 6147, 'UDP': 6143, 'SCTP': 91, 'DCCP': 10} total = 0 + aliased = 0 # the suffix branch of both dunders; only TCP and UDP have any for name, cls in registries.items(): - count = 0 - for member in cls: - count += 1 - total += 1 + count = len(list(cls)) + total += count + sample = _dunder_sample(cls) + aliased += sum(1 for member in sample if member.aliases) + for member in sample: svc, port, proto = member.svc, member.port, member.proto with self.subTest(registry=name, member=member.name, check='value'): @@ -1252,6 +1280,7 @@ def test_the_new_dunders_are_byte_identical_to_the_percent_form(self) -> None: self.assertEqual(count, expected_counts[name], f'{name} population changed') self.assertEqual(total, 12391) + self.assertGreater(aliased, 0, 'the sample lost the aliased branch') def test_every_member_renders_its_own_registrys_transport_protocol(self) -> None: """GitHub issue #806, member by member over all 12,391. diff --git a/tests/const/test_const_enum_builtin_parity.py b/tests/const/test_const_enum_builtin_parity.py index b1079ddfe..fe8f0f76d 100644 --- a/tests/const/test_const_enum_builtin_parity.py +++ b/tests/const/test_const_enum_builtin_parity.py @@ -58,7 +58,8 @@ import aenum -from tests._support import ISOLATED_PREFIXES, purge_modules, restore_modules, snapshot_modules +from tests._support import (ISOLATED_PREFIXES, purge_modules, reimport_once_per_class, + restore_modules, snapshot_modules) if TYPE_CHECKING: from typing import Optional @@ -546,7 +547,7 @@ class ConstFlagCompositeTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_tcp_flag_composites_resolve(self) -> None: from pcapkit.const.tcp.flags import Flags @@ -675,6 +676,10 @@ class ConstEnumRegisterFallbackTests(unittest.TestCase): """ def setUp(self) -> None: + # Per test, unlike most classes since GitHub issue #1065: these read the + # registries' lookup tables, which any earlier lookup in the same import + # can grow -- ``Flags(0)`` anywhere caches ``0`` as a pseudo-member, and + # ``Flags.get(UNRESOLVABLE, 0)`` then resolves instead of raising. purge_modules(['pcapkit']) def tearDown(self) -> None: @@ -909,7 +914,7 @@ class ConstEnumGuardTemplateTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @unittest.skipUnless(importlib.util.find_spec('requests') is not None, 'pcapkit.vendor needs requests') diff --git a/tests/const/test_const_enum_lookup.py b/tests/const/test_const_enum_lookup.py index b072318a7..8ba603e34 100644 --- a/tests/const/test_const_enum_lookup.py +++ b/tests/const/test_const_enum_lookup.py @@ -41,9 +41,9 @@ this module carries a companion sweep over the :class:`~aenum.IntFlag` classes, covering the contract the :class:`~aenum.IntEnum` sweep declines to. -Every test class purges :mod:`pcapkit` from :data:`sys.modules` in ``setUp``, +Every test class re-imports :mod:`pcapkit` for itself from ``setUp``, matching the convention every other module in this suite uses (see -:func:`tests._support.purge_modules`). It matters more here than usual: this +:func:`tests._support.reimport_once_per_class`). It matters more here than usual: this module is the first thing in the whole suite to import *every* submodule under :mod:`pcapkit.const`, including ones nothing else touches (e.g. :mod:`pcapkit.const.reg.apptype`). ``tests/cli/test_main.py`` stubs pieces of @@ -69,7 +69,8 @@ from aenum import IntEnum, IntFlag -from tests._support import ISOLATED_PREFIXES, purge_modules, restore_modules, snapshot_modules +from tests._support import (ISOLATED_PREFIXES, purge_modules, reimport_once_per_class, + restore_modules, snapshot_modules) #: Fully qualified names of the :class:`~aenum.IntEnum` classes under #: :mod:`pcapkit.const` for which rejecting ``0`` is *correct*, because their @@ -249,7 +250,7 @@ class ConstMissingClassmethodTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_every_missing_is_a_classmethod(self) -> None: offenders = [] # type: list[str] @@ -441,7 +442,7 @@ class RouterAlertPacketParseTests(unittest.TestCase): """Parse an on-the-wire packet carrying RFC 2113's Router Alert value 0.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_parses_igmp_over_router_alert_zero(self) -> None: from pcapkit.protocols.internet.ipv4 import IPv4 diff --git a/tests/const/test_const_enum_no_mint.py b/tests/const/test_const_enum_no_mint.py index 437c38f43..29089a6fc 100644 --- a/tests/const/test_const_enum_no_mint.py +++ b/tests/const/test_const_enum_no_mint.py @@ -176,7 +176,8 @@ import unittest from typing import TYPE_CHECKING -from tests._support import ISOLATED_PREFIXES, purge_modules, restore_modules, snapshot_modules +from tests._support import (ISOLATED_PREFIXES, purge_modules, reimport_once_per_class, + restore_modules, snapshot_modules) if TYPE_CHECKING: from typing import Optional @@ -774,9 +775,7 @@ class GetNoLongerMintsTests(unittest.TestCase): same ruling applied to the int path above.""" def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_unresolvable_string_key_without_default_raises(self) -> None: from pcapkit.const.arp.hardware import Hardware @@ -818,9 +817,7 @@ class RegisterStillMintsTests(unittest.TestCase): """The one caller-named, explicit path must still grow the registry.""" def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_register_mints_a_real_permanent_member(self) -> None: from pcapkit.const.arp.hardware import Hardware @@ -882,9 +879,7 @@ class UnregisteredMemberHelperTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_builds_an_absent_member_on_every_registry(self) -> None: value = 0x6E7A0002 # arbitrary, distinct from the register() sweep's @@ -1504,9 +1499,7 @@ class EtherTypeMixedMintTests(unittest.TestCase): rather than renamed, so history stays easy to follow).""" def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_unassigned_rows_do_not_mint(self) -> None: from pcapkit.const.reg.ethertype import EtherType @@ -1572,9 +1565,7 @@ class IPXSocketMixedMintTests(unittest.TestCase): :meth:`test_registered_by_xerox_no_longer_mints` below.""" def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_unassigned_rows_do_not_mint(self) -> None: from pcapkit.const.ipx.socket import Socket @@ -1651,9 +1642,7 @@ class therefore overrides :meth:`_unregistered_member` to reconstruct """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_unassigned_value_resolves_without_minting(self) -> None: """Swept across all 5: resolves, does not mint, repeated lookup is @@ -1907,9 +1896,7 @@ class BespokeOpenVocabularyUnmintConvertedTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_featcode_import_mints_nothing(self) -> None: """The guard against the import-time mutation coming back -- count- @@ -2185,9 +2172,7 @@ class BespokeGetReplacementTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_statuscode_get_omitted_default_still_raises(self) -> None: """The one production call site's shape: no default passed, so an @@ -2258,7 +2243,10 @@ class BespokeGetUnchangedTests(unittest.TestCase): case-insensitively (GitHub issue #582 -- ``Command.get('abor')`` must return :attr:`Command.ABOR`, not raise), which the base's plain ``_member_map_``/``_value2member_map_`` lookup does not do -- swapping in - the base would silently reintroduce #582. + the base would silently reintroduce #582. That pin lives in + :mod:`tests.const.test_const_method_case_sensitive_896_unit` + (``test_command_get_stays_case_insensitive``), whose body this class used + to repeat verbatim. :class:`Method` used to resolve case-insensitively the same way (#583), but GitHub issue #896 retired that: RFC 9110 Section 9.1 makes the HTTP @@ -2281,16 +2269,7 @@ class BespokeGetUnchangedTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) - - def test_command_get_is_still_case_insensitive(self) -> None: - from pcapkit.const.ftp.command import Command - - for key in ('RETR', 'retr', 'ReTr', 'rEtR'): - with self.subTest(key=key): - self.assertIs(Command.get(key), Command.RETR) # type: ignore[attr-defined] + reimport_once_per_class(self, restore=True) def test_method_get_is_now_case_sensitive(self) -> None: """GitHub issue #896: only the exact registered casing resolves. @@ -2404,9 +2383,7 @@ class AppTypeUnmintConvertedTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_apptype_family_carries_the_registry_protocol(self) -> None: """#842's ruling is that ``get``/``get_all``/``register``/ diff --git a/tests/const/test_const_ethertype_862_unit.py b/tests/const/test_const_ethertype_862_unit.py index 3efecd4bc..67516964f 100644 --- a/tests/const/test_const_ethertype_862_unit.py +++ b/tests/const/test_const_ethertype_862_unit.py @@ -54,7 +54,7 @@ import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class #: CSV fixture reproducing IANA's own rows for the #862 pair plus one #: unrelated, non-overlapping range, byte-for-byte (captured from a live fetch @@ -75,7 +75,7 @@ class EtherType862ConstResolutionTests(unittest.TestCase): """Against the generated, committed :class:`~pcapkit.const.reg.ethertype.EtherType`.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_value_0x0101_resolves_to_old_xerox_name(self) -> None: from pcapkit.const.reg.ethertype import EtherType @@ -119,7 +119,7 @@ class EtherTypeGeneratorRangeOrderingTests(unittest.TestCase): """Against the generator's own ``process()``, root cause rather than symptom.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @staticmethod def _process_fixture() -> 'tuple[list[str], list[str]]': @@ -202,7 +202,7 @@ class EtherTypeGeneratorGeneralOrderingTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_nested_ranges_are_ordered_narrowest_first(self) -> None: """Regression pin against two specific wrong implementations of diff --git a/tests/const/test_const_linktype_209_unit.py b/tests/const/test_const_linktype_209_unit.py index 9a06252f2..3b2c70714 100644 --- a/tests/const/test_const_linktype_209_unit.py +++ b/tests/const/test_const_linktype_209_unit.py @@ -70,7 +70,7 @@ import importlib.util import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class #: The crawler dependencies :class:`LinkTypeGeneratorLegacyOrderingTests` needs to #: parse its fixture. Spelled exactly as @@ -128,7 +128,7 @@ class LinkType209ConstResolutionTests(unittest.TestCase): """Against the generated, committed :class:`~pcapkit.const.reg.linktype.LinkType`.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_value_209_resolves_to_the_current_name(self) -> None: from pcapkit.const.reg.linktype import LinkType @@ -153,7 +153,7 @@ class LinkTypeGeneratorLegacyOrderingTests(unittest.TestCase): """Against the generator's own ``process()``, root cause rather than symptom.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @staticmethod def _process_fixture() -> 'list[str]': @@ -288,7 +288,7 @@ class LinkTypeGeneratorValueAwareTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @staticmethod def _process(html: str) -> 'list[str]': @@ -401,7 +401,7 @@ class LinkTypeGeneratorRangeSinkIsolationTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_legacy_worded_range_row_still_lands_entirely_in_enum(self) -> None: import bs4 @@ -476,7 +476,7 @@ class LinkTypeGeneratorCrossRangeDuplicateTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_legacy_row_duplicating_a_range_member_is_sunk(self) -> None: import bs4 @@ -590,7 +590,7 @@ class LinkTypeGeneratorSignedOrUnderscoredValueTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @staticmethod def _process(html: str) -> 'list[str]': diff --git a/tests/const/test_const_method_case_sensitive_896_unit.py b/tests/const/test_const_method_case_sensitive_896_unit.py index 12c016e49..75dcc6e51 100644 --- a/tests/const/test_const_method_case_sensitive_896_unit.py +++ b/tests/const/test_const_method_case_sensitive_896_unit.py @@ -73,7 +73,7 @@ import re import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class def _reconstruct_fixture_csv() -> 'str': @@ -156,7 +156,7 @@ class MethodGetCaseSensitivityTests(unittest.TestCase): """The direct repro and fix for GitHub issue #896.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_get_exact_case_resolves_the_standardised_member(self) -> None: """The unaffected half: the registered, all-uppercase casing every @@ -217,6 +217,10 @@ def test_command_get_stays_case_insensitive(self) -> None: :meth:`~pcapkit.const.ftp.command.Command.get` keeps folding case -- pinned here so a future edit to the sibling ``Method`` fix cannot silently carry the case-sensitive change over to ``Command`` too. + It is also the only pin of GitHub issue #582 (``Command.get('abor')`` + must resolve), which + :class:`tests.const.test_const_enum_no_mint.BespokeGetUnchangedTests` + relies on rather than repeating. """ from pcapkit.const.ftp.command import Command diff --git a/tests/const/test_const_registry_protocol.py b/tests/const/test_const_registry_protocol.py index e69339fdf..bda6a6639 100644 --- a/tests/const/test_const_registry_protocol.py +++ b/tests/const/test_const_registry_protocol.py @@ -29,7 +29,8 @@ from aenum import IntEnum, IntFlag, StrEnum, extend_enum from pcapkit.corekit.enum import EnumRegistry -from tests._support import ISOLATED_PREFIXES, purge_modules, restore_modules, snapshot_modules +from tests._support import (ISOLATED_PREFIXES, purge_modules, reimport_once_per_class, + restore_modules, snapshot_modules) if TYPE_CHECKING: from typing import Any @@ -252,16 +253,14 @@ class TCPFlagsConversionTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_tcp_flags_now_inherits_the_base(self) -> None: """Fails on ``02296b5dd``, where ``Flags`` is a plain ``IntFlag``. Resolves ``EnumRegistry`` freshly from the just-reimported ``pcapkit.corekit.enum`` rather than the module-scope import above -- - ``setUp`` purged ``pcapkit`` from ``sys.modules``, so ``Flags`` now + ``setUp`` re-imported ``pcapkit`` for this class, so ``Flags`` now inherits a *new* ``EnumRegistry`` class object, and identity against the stale outer one would fail for a reason that says nothing about the product (see ``ProtocolIsInheritedTests.setUpClass`` above, which @@ -328,9 +327,7 @@ class GetContractTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_name_and_value_both_resolve_to_the_same_member(self) -> None: from pcapkit.const.ipv6.extension_header import ExtensionHeader @@ -406,9 +403,7 @@ class GetAllContractTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_a_one_to_one_registry_matches_exactly_one_member(self) -> None: """An alias is a second *name* for the canonical member, not a second @@ -447,9 +442,7 @@ class RegisterContractTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_register_adds_a_new_member_under_the_given_name(self) -> None: from pcapkit.const.ipv6.extension_header import ExtensionHeader @@ -589,9 +582,7 @@ class RegisterAliasContractTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_alias_adds_a_name_not_a_member(self) -> None: from pcapkit.const.ipv6.extension_header import ExtensionHeader @@ -654,9 +645,7 @@ class UnregisteredMemberTests(unittest.TestCase): member types the base serves.""" def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_int_valued_registries(self) -> None: for module_name, class_name, _ in CONVERTED: @@ -932,9 +921,7 @@ class GeneratedMissingRangeParityTests(unittest.TestCase): regression pin on that claim rather than a test of new behaviour.""" def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_hardware_still_bounds_and_resolves_its_unassigned_ranges(self) -> None: """:class:`~pcapkit.const.arp.hardware.Hardware` declares ``0..38`` @@ -1011,9 +998,7 @@ class RegisterAlreadyRegisteredNowRaisesOnAGeneratedRegistryTests(unittest.TestC """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_register_over_a_taken_value_now_raises(self) -> None: from pcapkit.const.reg.transtype import TransType @@ -1071,9 +1056,7 @@ class GetDispatchMatrixTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_valid_int_value(self) -> None: from pcapkit.const.reg.transtype import TransType @@ -1138,9 +1121,7 @@ class GetDispatchMatrixIntFlagTests(unittest.TestCase): the ``IntFlag`` path really is untouched, not to assume it.""" def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_valid_int_value(self) -> None: from pcapkit.const.mh.handover_ack_flag import HandoverACKFlag @@ -1340,9 +1321,7 @@ class NoDefaultSentinelTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_no_default_is_not_equal_to_any_plausible_caller_value(self) -> None: """The sentinel's whole point: unlike ``-1``, a @@ -1421,66 +1400,55 @@ def test_identity_survives_an_ordinary_second_import(self) -> None: self.assertIs(module_again.NO_DEFAULT, first_import) def test_missing_name_with_default_negative_one_is_now_a_real_default(self) -> None: - """Coincidentally passes on the pre-#857 tree too, not just this one: - ``ExtensionHeader.get(, -1)`` raised :exc:`KeyError` there - as well, because ``-1 == NO_DEFAULT`` read the caller's ``-1`` as *no - default* and re-raised the name lookup's own error -- the same - exception type and content this test asserts, but for the wrong - reason (silently discarding an explicit default rather than - genuinely attempting and failing to resolve it). This test no longer - discriminates that defect; the pin for it is - :meth:`NoDefaultSentinelTests.test_no_default_is_not_equal_to_any_plausible_caller_value`, - which checks the sentinel's identity comparison directly, never - touches :meth:`~pcapkit.corekit.enum.EnumRegistry.get`, and so is - unaffected by #864 -- look there, not here, for that discrimination. - - ``-1`` is a genuine default here, not a rediscovered sentinel; it is - simply not a *resolvable* one, since #864 restricts - ``default`` to :attr:`~pcapkit.corekit.enum.EnumRegistry._value2member_map_`, - every registry's domain starts at ``0``, and there is no - ``cls(default)`` fallback left to attempt and raise a fresh - :exc:`ValueError` from. So the original name lookup's own - :exc:`KeyError` propagates instead, exactly as it would with no - default at all. Updated from the pre-#864 tree, which asserted - :exc:`ValueError` mentioning ``-1`` and not the original key -- that - assertion described ``cls(-1)`` failing, a code path #864 removes; it - is not weakened here, it is retargeted at the deliberate replacement - contract, and still pins that ``-1`` never mints anything. + """``-1`` and ``-1.0`` are honoured as defaults by + :meth:`~pcapkit.corekit.enum.EnumRegistry.get` itself. + + This used to probe :class:`~pcapkit.const.ipv6.extension_header.ExtensionHeader`, + and its docstring conceded that it no longer discriminated #857: no + shipped registry's domain reaches ``-1``, so since #864 the name + lookup's :exc:`KeyError` propagates whether ``-1`` is read as a default + or as the old marker. A throwaway registry holding ``-1`` tells the two + apart again: it resolves to that member now, while the pre-#857 + ``default == -1`` check read the caller's ``-1`` (and ``-1.0``, which + compares equal) as *no default* and raised (GitHub issue #1062). """ - from pcapkit.const.ipv6.extension_header import ExtensionHeader + from aenum import IntEnum - before = len(ExtensionHeader.__members__) - with self.assertRaises(KeyError) as caught: - ExtensionHeader.get('Definitely-Not-A-Member', -1) - self.assertIn('Definitely-Not-A-Member', str(caught.exception)) - self.assertEqual(before, len(ExtensionHeader.__members__)) + from pcapkit.corekit.enum import EnumRegistry - def test_missing_name_with_default_negative_one_float_is_now_a_real_default(self) -> None: - """The float case that actually motivates #857: ``-1.0 == -1`` is - ``True``, so the old ``==`` comparison could not tell a caller's - ``-1.0`` apart from the ``-1`` marker either. Coincidentally passes - on the pre-#857 tree too, for the same reason its sibling test above - explains -- the old comparison read ``-1.0`` as the sentinel there, - producing the very same :exc:`KeyError` this asserts, for the wrong - reason. The discriminating pin for #857 lives in - :meth:`NoDefaultSentinelTests.test_no_default_is_not_equal_to_any_plausible_caller_value` - instead, not here -- see that sibling test's docstring for why. - - Raises :exc:`KeyError` here, post-#864, for the reason its sibling - test above explains: ``-1.0`` is not a registered value, ``default`` - no longer reaches ``cls(default)`` to fail on its own terms, and the - original name lookup's error propagates instead. Updated from the - pre-#864 tree, which asserted :exc:`ValueError` mentioning ``-1.0`` - for ``ExtensionHeader(-1.0)`` -- that path no longer exists; this is - the same no-weaker retargeting as above. + class Signed(EnumRegistry, IntEnum): + MINUS_ONE = -1 + ZERO = 0 + + for default in (-1, -1.0): + with self.subTest(default=default): + self.assertIs(Signed.get('Definitely-Not-A-Member', default), Signed.MINUS_ONE) + with self.assertRaises(KeyError): + Signed.get('Definitely-Not-A-Member') + self.assertEqual(len(Signed.__members__), 2) + + def test_negative_one_defaults_on_a_shipped_registry_raise_and_mint_nothing(self) -> None: + """The #864 contract for ``-1`` and ``-1.0`` on a real registry. + + Neither is a registered value of any shipped registry, whose domains + all start at ``0``, so ``default`` does not resolve: there is no + ``cls(default)`` fallback left to attempt, and the name lookup's own + :exc:`KeyError` propagates, as with no default at all. Pinned from the + pre-#864 tree, which raised :exc:`ValueError` from ``cls(-1)``. This + does *not* discriminate #857, which raised the same :exc:`KeyError`; + :meth:`test_missing_name_with_default_negative_one_is_now_a_real_default` + does. Merged from separate ``-1`` and ``-1.0`` tests (GitHub issue + #1062). """ from pcapkit.const.ipv6.extension_header import ExtensionHeader - before = len(ExtensionHeader.__members__) - with self.assertRaises(KeyError) as caught: - ExtensionHeader.get('Definitely-Not-A-Member', -1.0) - self.assertIn('Definitely-Not-A-Member', str(caught.exception)) - self.assertEqual(before, len(ExtensionHeader.__members__)) + for default in (-1, -1.0): + with self.subTest(default=default): + before = len(ExtensionHeader.__members__) + with self.assertRaises(KeyError) as caught: + ExtensionHeader.get('Definitely-Not-A-Member', default) + self.assertIn('Definitely-Not-A-Member', str(caught.exception)) + self.assertEqual(before, len(ExtensionHeader.__members__)) def test_missing_name_without_default_still_raises_on_an_int_enum(self) -> None: """Omitting ``default`` entirely is unaffected by this change -- it @@ -1525,9 +1493,7 @@ class GetDefaultNoMintTests(unittest.TestCase): """ def setUp(self) -> None: - snapshot = snapshot_modules(ISOLATED_PREFIXES) - purge_modules(['pcapkit']) - self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + reimport_once_per_class(self, restore=True) def test_the_issues_own_case_no_longer_mints_and_raises_instead(self) -> None: """The exact reproduction from #864, on the real shipped registry. diff --git a/tests/const/test_const_str_payload_870_unit.py b/tests/const/test_const_str_payload_870_unit.py index bd0e361ec..cae756933 100644 --- a/tests/const/test_const_str_payload_870_unit.py +++ b/tests/const/test_const_str_payload_870_unit.py @@ -60,14 +60,14 @@ import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class class MethodStrPayload870RegressionTests(unittest.TestCase): """The exact repro from GitHub issue #870, and the full sweep of Method's own 40 members.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_the_reported_case(self) -> None: """``Method.GET``, byte for byte, as the issue measured it. @@ -141,7 +141,7 @@ class StrValuedRegistryPayloadTests(unittest.TestCase): SWEPT_REGISTRIES = ('Command', 'FEATCode', 'Method') def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_every_member_of_every_swept_registry_matches_its_value(self) -> None: from pcapkit.const.ftp.command import Command, FEATCode diff --git a/tests/corekit/test_fields_collections.py b/tests/corekit/test_fields_collections.py index 8606b9b29..2bd7e48bc 100644 --- a/tests/corekit/test_fields_collections.py +++ b/tests/corekit/test_fields_collections.py @@ -4,7 +4,7 @@ import unittest from typing import TYPE_CHECKING -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -22,7 +22,7 @@ class OptionFieldTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) from pcapkit.corekit.fields.collections import OptionField from pcapkit.corekit.fields.numbers import UInt16Field @@ -130,7 +130,7 @@ class OptionFieldForeignBaseSchemaTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) #: Every schema unpacked while parsing an option list, in order. The base #: schema appearing here is what distinguishes the full unpack from the @@ -287,7 +287,7 @@ class OptionFieldPackageDeclarationTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_no_declaration_in_the_package_falls_back(self) -> None: import pcapkit.all # noqa: F401 (realises every schema declaration) @@ -302,6 +302,11 @@ def subclasses(cls: 'Any') -> 'Any': fell_back = [] # type: list[str] total = 0 for cls in set(subclasses(Schema)): + # Only this package's own declarations: a test that builds a schema + # of its own leaves it among the subclasses for as long as the + # import lives, and that is no longer one import per test (#1065). + if cls.__module__.split('.')[0] != 'pcapkit': + continue for name, field in getattr(cls, '__fields__', {}).items(): if not isinstance(field, OptionField): continue @@ -332,7 +337,7 @@ class ListFieldSchemaItemTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_a_length_callback_is_honoured(self) -> None: """The per-item field's own resolved length must reach its schema. diff --git a/tests/corekit/test_fields_copy_dispatch_runtime.py b/tests/corekit/test_fields_copy_dispatch_runtime.py index 5cc2b07f3..11e3a430e 100644 --- a/tests/corekit/test_fields_copy_dispatch_runtime.py +++ b/tests/corekit/test_fields_copy_dispatch_runtime.py @@ -37,7 +37,7 @@ import importlib.util import unittest -from tests._support import purge_modules, sample_path +from tests._support import reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -92,7 +92,7 @@ class FieldCopyDispatchByteIdentityTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def assertCaptureRoundTripsByteForByte(self, name: str) -> None: """Walk every record of ``name`` and compare its repack to the file's own bytes. diff --git a/tests/corekit/test_fields_copy_dispatch_unit.py b/tests/corekit/test_fields_copy_dispatch_unit.py index 6459ec61f..d05574863 100644 --- a/tests/corekit/test_fields_copy_dispatch_unit.py +++ b/tests/corekit/test_fields_copy_dispatch_unit.py @@ -34,7 +34,7 @@ from typing import TYPE_CHECKING from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -53,7 +53,7 @@ class FieldCallUsesCopyDunderDirectlyTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) from pcapkit.corekit.fields.field import FieldBase diff --git a/tests/corekit/test_fields_field.py b/tests/corekit/test_fields_field.py index c0531a0ec..241b15bdd 100644 --- a/tests/corekit/test_fields_field.py +++ b/tests/corekit/test_fields_field.py @@ -4,7 +4,7 @@ import threading import unittest -from tests._support import purge_modules, time_limit +from tests._support import reimport_once_per_class, time_limit class FieldBaseUnpackBoundsTests(unittest.TestCase): @@ -42,7 +42,7 @@ class FieldBaseUnpackBoundsTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) from pcapkit.corekit.fields import field as field_module from pcapkit.corekit.fields.strings import BytesField @@ -225,7 +225,7 @@ class FieldBaseCumulativePaddingBudgetTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) from pcapkit.corekit.fields import field as field_module from pcapkit.corekit.fields.strings import BytesField @@ -236,6 +236,14 @@ def setUp(self) -> None: self.FieldValueError = FieldValueError self.ceiling = field_module._MAX_ZERO_PAD_LENGTH + # The ledger is cumulative per context, so whatever ran earlier in this + # process -- a sibling class, another module -- has already earned + # allowance against it. Start every test from the unset default, as a + # freshly imported module would, rather than relying on a re-import per + # test to do it (GitHub issue #1065). + token = field_module._zero_pad_ledger.set(None) + self.addCleanup(field_module._zero_pad_ledger.reset, token) + # NOTE: deliberately *not* read off the module. The behavioural tests # below have to fail on their assertions when the budget is absent, not # on an :exc:`AttributeError` raised here before any of them runs -- a @@ -596,7 +604,7 @@ class FieldBaseShortReadPaddingSideTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) from pcapkit.corekit.fields.numbers import (Int32Field, UInt16Field, UInt32Field, UInt64Field) @@ -877,7 +885,7 @@ class FieldBaseLengthNegativeResolvedLengthTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) from pcapkit.corekit.fields.numbers import NumberField from pcapkit.corekit.fields.strings import BytesField diff --git a/tests/corekit/test_fields_ipaddress.py b/tests/corekit/test_fields_ipaddress.py index affb2b592..845a9668a 100644 --- a/tests/corekit/test_fields_ipaddress.py +++ b/tests/corekit/test_fields_ipaddress.py @@ -4,7 +4,7 @@ import ipaddress import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -26,7 +26,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class IPAddressFieldTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_address_fields_round_trip_and_reject_the_other_version(self) -> None: from pcapkit.corekit.fields.ipaddress import IPv4AddressField, IPv6AddressField diff --git a/tests/corekit/test_fields_misc.py b/tests/corekit/test_fields_misc.py index 645b51efe..ef07facb4 100644 --- a/tests/corekit/test_fields_misc.py +++ b/tests/corekit/test_fields_misc.py @@ -3,7 +3,7 @@ import copy import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class #: Every address-typed branch of every address-typed #: :class:`~pcapkit.corekit.fields.misc.SwitchField` in the schema tree, as @@ -137,7 +137,7 @@ class SchemaFieldDefaultTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) from pcapkit.corekit.fields.misc import SchemaField from pcapkit.corekit.fields.numbers import UInt8Field @@ -187,7 +187,7 @@ class SwitchFieldBoolDispatchTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @staticmethod def _resolve(module: 'str', name: 'str', attr: 'str') -> 'tuple[object, object]': @@ -418,7 +418,7 @@ class PayloadFieldProtocolNameTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_a_protocol_name_resolves_whatever_its_case(self) -> None: from pcapkit.corekit.fields.misc import PayloadField diff --git a/tests/corekit/test_fields_misc_packet_context.py b/tests/corekit/test_fields_misc_packet_context.py index 5eaf5082b..efd950834 100644 --- a/tests/corekit/test_fields_misc_packet_context.py +++ b/tests/corekit/test_fields_misc_packet_context.py @@ -5,7 +5,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -58,7 +58,7 @@ class NestedPacketContextSemanticsTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _make_schema_classes(self): """Outer/Inner pair mirroring ``CGAParametersOption``/``CGAParameter``. @@ -247,7 +247,7 @@ class CGAParametersRegressionTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_cga_parameters_option_now_parses_end_to_end(self) -> None: from pcapkit.const.mh.option import Option as Enum_Option diff --git a/tests/corekit/test_fields_numbers.py b/tests/corekit/test_fields_numbers.py index d76be8bae..792d1a76f 100644 --- a/tests/corekit/test_fields_numbers.py +++ b/tests/corekit/test_fields_numbers.py @@ -4,7 +4,7 @@ import inspect import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -38,7 +38,7 @@ class FixedSignTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_the_census_is_complete(self) -> None: """:data:`FIXED_SIGN` is every subclass that fixes a sign, by introspection.""" @@ -125,7 +125,7 @@ class DeferredSignTests(unittest.TestCase): """``signed=`` where no ``__signed__`` fixes it, which is where it works.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_the_base_class_still_honours_signed(self) -> None: from pcapkit.corekit.fields.numbers import NumberField diff --git a/tests/corekit/test_fields_numbers_callable_length.py b/tests/corekit/test_fields_numbers_callable_length.py index 66f588c86..f974622f8 100644 --- a/tests/corekit/test_fields_numbers_callable_length.py +++ b/tests/corekit/test_fields_numbers_callable_length.py @@ -22,7 +22,7 @@ import struct import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -51,7 +51,7 @@ class CallableLengthTests(unittest.TestCase): """The reported defect, and the widths either side of it.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_the_reported_case_a_callable_resolving_to_eight_packs(self) -> None: """``NumberField(length=lambda pkt: 8)`` could not pack at all. @@ -229,7 +229,7 @@ class SubclassCallableLengthTests(unittest.TestCase): """The same field classes as they are actually used in the schemas.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_an_enum_field_with_a_callable_length_packs(self) -> None: """:class:`~pcapkit.corekit.fields.numbers.EnumField` leaves ``__template__`` unset. diff --git a/tests/corekit/test_fields_numbers_negative_length.py b/tests/corekit/test_fields_numbers_negative_length.py index 80ca03703..bec60ef69 100644 --- a/tests/corekit/test_fields_numbers_negative_length.py +++ b/tests/corekit/test_fields_numbers_negative_length.py @@ -27,7 +27,7 @@ import importlib.util import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -40,7 +40,7 @@ class NegativeResolvedLengthTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_a_literal_negative_length_raises_protocolerror(self) -> None: """The narrowest reproduction: ``length=-1`` supplied directly. @@ -199,7 +199,7 @@ class NegativeBitLengthTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_a_negative_bit_length_raises_protocolerror(self) -> None: """The narrowest reproduction: ``bit_length=-1`` supplied directly. 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 3c19e6c33..4af7c6a34 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 @@ -47,7 +47,7 @@ import aenum -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -89,7 +89,7 @@ class PortEnumFieldBoundedFallbackTests(unittest.TestCase): """TCP, UDP and SCTP's port fields.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _fields(self) -> 'list[tuple[str, Any, Any]]': from pcapkit.const.reg.apptype import AppType, TransportProtocol @@ -305,7 +305,7 @@ class OptionEnumFieldBoundedFallbackTests(unittest.TestCase): """PCAP-NG's option-type field.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_an_undeclared_option_code_resolves_without_growing_the_registry(self) -> None: """An interface-block option code no ``if`` or ``opt`` row covers. @@ -445,7 +445,7 @@ class UnregisteredMemberRoundTripTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _resolved(self) -> 'Any': """An unassigned TCP port, resolved through the real field.""" diff --git a/tests/corekit/test_fields_numbers_unassigned_enum.py b/tests/corekit/test_fields_numbers_unassigned_enum.py index c1ae17d8e..1ec7544b7 100644 --- a/tests/corekit/test_fields_numbers_unassigned_enum.py +++ b/tests/corekit/test_fields_numbers_unassigned_enum.py @@ -67,7 +67,7 @@ import tempfile import unittest -from tests._support import close_extractor, purge_modules +from tests._support import close_extractor, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -122,7 +122,7 @@ class UnassignedEnumFieldTests(unittest.TestCase): """The field layer, in isolation from any protocol.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_an_unassigned_registry_value_resolves_instead_of_raising(self) -> None: """The reported lookup, as a field unpack. @@ -370,7 +370,7 @@ class UnassignedBlockTypeExtractionTests(unittest.TestCase): """The consequence the issue was filed about, end to end.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _protochains(self, path: str) -> 'list[str]': """Extract a capture and report each frame's protocol chain. diff --git a/tests/corekit/test_fields_numbers_width_repair.py b/tests/corekit/test_fields_numbers_width_repair.py index c81e64c91..e363a6dd4 100644 --- a/tests/corekit/test_fields_numbers_width_repair.py +++ b/tests/corekit/test_fields_numbers_width_repair.py @@ -40,7 +40,7 @@ import math import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -77,7 +77,7 @@ class WidthRepairBoundaryTests(unittest.TestCase): """One method per octet boundary, each asserting the pair across it.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_the_one_octet_boundary_needs_two_octets_past_255(self) -> None: """``255`` fits one octet; ``256`` needs two and was given one. @@ -251,7 +251,7 @@ class WidthRepairReachabilityTests(unittest.TestCase): """How a caller gets to the repair at all, which bounds what this fix touches.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_resolving_the_field_bypasses_the_repair_entirely(self) -> None: """The repair is a net under the *unresolved* field, nothing more. @@ -314,7 +314,7 @@ class WidthRepairSurroundingContractTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_a_field_with_no_length_at_all_is_refused_outright(self) -> None: """Ruling out the other way the placeholder might have been thought to arise. diff --git a/tests/corekit/test_fields_strings.py b/tests/corekit/test_fields_strings.py index a5e4b07ee..45d467ca6 100644 --- a/tests/corekit/test_fields_strings.py +++ b/tests/corekit/test_fields_strings.py @@ -2,14 +2,14 @@ import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class class BitFieldTests(unittest.TestCase): """Packing and parsing of :class:`~pcapkit.corekit.fields.strings.BitField`.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) from pcapkit.corekit.fields.strings import BitField diff --git a/tests/corekit/test_infoclass.py b/tests/corekit/test_infoclass.py index b839674a5..8825e7ebd 100644 --- a/tests/corekit/test_infoclass.py +++ b/tests/corekit/test_infoclass.py @@ -4,7 +4,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -13,7 +13,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class InfoClassTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_finalised_annotated_info_generates_init_and_warns_once_final(self) -> None: from pcapkit.corekit.infoclass import Info, info_final diff --git a/tests/corekit/test_io.py b/tests/corekit/test_io.py index 1b29ffafb..76b998bc1 100644 --- a/tests/corekit/test_io.py +++ b/tests/corekit/test_io.py @@ -7,7 +7,7 @@ import unittest from unittest import mock -from tests._support import bootstrap_core_modules, load_module, purge_modules +from tests._support import bootstrap_core_modules, load_module, reimport_once_per_class class SeekableReaderTests(unittest.TestCase): @@ -16,7 +16,8 @@ def _close_reader(self, reader) -> None: self.assertTrue(reader.closed) def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + modules = bootstrap_core_modules() self.exceptions = modules['exceptions'] self.io_module = load_module('pcapkit.corekit.io', 'pcapkit/corekit/io.py') diff --git a/tests/corekit/test_module.py b/tests/corekit/test_module.py index 73c870f8e..e04df41db 100644 --- a/tests/corekit/test_module.py +++ b/tests/corekit/test_module.py @@ -7,12 +7,13 @@ import unittest from unittest import mock -from tests._support import load_module, purge_modules +from tests._support import load_module, reimport_once_per_class class ModuleDescriptorTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + self.module = load_module('pcapkit.corekit.module', 'pcapkit/corekit/module.py') def _register(self, name: str, module: types.ModuleType) -> None: diff --git a/tests/corekit/test_multidict.py b/tests/corekit/test_multidict.py index 353fd2662..605c32b53 100644 --- a/tests/corekit/test_multidict.py +++ b/tests/corekit/test_multidict.py @@ -3,12 +3,13 @@ import copy import unittest -from tests._support import bootstrap_core_modules, purge_modules +from tests._support import bootstrap_core_modules, reimport_once_per_class class MultiDictTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + modules = bootstrap_core_modules() self.multidict = modules['multidict'] self.exceptions = modules['exceptions'] diff --git a/tests/dumpkit/test_common_unit.py b/tests/dumpkit/test_common_unit.py index 1d5ebdfcd..f280dd154 100644 --- a/tests/dumpkit/test_common_unit.py +++ b/tests/dumpkit/test_common_unit.py @@ -14,7 +14,7 @@ from unittest import mock import warnings -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -99,7 +99,7 @@ def __init__(self, *args: 'object', **kwargs: 'object') -> None: # pylint: disa @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class DumpkitCommonTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_dumper_subclass_registration_is_opt_in(self) -> None: """Registration happens if and only if ``fmt`` is given. @@ -399,7 +399,7 @@ def test_an_undeclared_option_code_dumps_its_addon_keys_in_order_too(self) -> No @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class DumpkitIOTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_null_dumper_noops_and_pcap_dumper_writes_file(self) -> None: from pcapkit.const.reg.linktype import LinkType diff --git a/tests/dumpkit/test_nameless_enum_rendering_unit.py b/tests/dumpkit/test_nameless_enum_rendering_unit.py index 5c347edc4..f7f476363 100644 --- a/tests/dumpkit/test_nameless_enum_rendering_unit.py +++ b/tests/dumpkit/test_nameless_enum_rendering_unit.py @@ -7,7 +7,7 @@ import unittest from typing import TYPE_CHECKING -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: import aenum @@ -114,7 +114,7 @@ class NamelessEnumRenderingTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_scalar_return_renders_a_nameless_member_as_its_value(self) -> None: """The plain ``return`` at the end of the enumeration branch. diff --git a/tests/dumpkit/test_plist_escaping_regression.py b/tests/dumpkit/test_plist_escaping_regression.py index 772c0b429..e08081f4c 100644 --- a/tests/dumpkit/test_plist_escaping_regression.py +++ b/tests/dumpkit/test_plist_escaping_regression.py @@ -28,7 +28,7 @@ import unittest import xml.etree.ElementTree as ET -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -51,7 +51,8 @@ class PlistKeyEscapingTests(unittest.TestCase): """The reports of :file:`test.pcapng`, one per output format.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + tmpdir = tempfile.TemporaryDirectory(prefix='pcapkit-772-') self.addCleanup(tmpdir.cleanup) self.tmp_path = pathlib.Path(tmpdir.name) diff --git a/tests/foundation/engines/test_engine_base.py b/tests/foundation/engines/test_engine_base.py index 4320a07eb..4310e4539 100644 --- a/tests/foundation/engines/test_engine_base.py +++ b/tests/foundation/engines/test_engine_base.py @@ -6,7 +6,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -15,7 +15,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class EngineBaseTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_engine_metadata_extractor_call_and_close(self) -> None: from pcapkit.foundation.engines.engine import EngineBase diff --git a/tests/foundation/engines/test_new_engine_parity_runtime.py b/tests/foundation/engines/test_new_engine_parity_runtime.py index 2dbf017bb..6a5272e8b 100644 --- a/tests/foundation/engines/test_new_engine_parity_runtime.py +++ b/tests/foundation/engines/test_new_engine_parity_runtime.py @@ -34,7 +34,7 @@ import unittest from unittest import mock -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -87,7 +87,7 @@ def mac(raw) -> str: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class NewEngineParityTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def extract(self, engine: str, capture: str, **kwargs): """Extract a capture, asserting that the requested engine really ran. diff --git a/tests/foundation/engines/test_pcap_ct_engine.py b/tests/foundation/engines/test_pcap_ct_engine.py index fb4ea2657..5ed95a605 100644 --- a/tests/foundation/engines/test_pcap_ct_engine.py +++ b/tests/foundation/engines/test_pcap_ct_engine.py @@ -26,7 +26,7 @@ import unittest from unittest import mock -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -131,7 +131,7 @@ def close(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PCAP_CTEngineTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) handle, path = tempfile.mkstemp(suffix='.pcap') os.close(handle) diff --git a/tests/foundation/engines/test_pcapng_engine.py b/tests/foundation/engines/test_pcapng_engine.py index e9c4c6e0d..263d46c2f 100644 --- a/tests/foundation/engines/test_pcapng_engine.py +++ b/tests/foundation/engines/test_pcapng_engine.py @@ -7,7 +7,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class from tests.foundation.engines.test_runtime_engines import FakeInfo, make_extractor RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') @@ -15,7 +15,7 @@ #: Link layer type 1, i.e. ``LinkType.ETHERNET``. Spelled as a literal because the #: captures below are assembled without importing :mod:`pcapkit`, which -#: :meth:`setUp` purges from :data:`sys.modules` before every test. +#: :meth:`setUp` re-imports for each class. LINKTYPE_ETHERNET = 1 #: Link layer type 101, i.e. ``LinkType.RAW``. Only used as a *second* interface, #: to tell which interface a Simple Packet Block was decoded against. @@ -103,7 +103,7 @@ def interface_statistics(self, interface_id: int = 0) -> PCAPNGWriter: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PCAPNGEngineTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _info(self, block_type, **kwargs) -> FakeInfo: return FakeInfo(type=block_type, **kwargs) @@ -339,7 +339,7 @@ class PCAPNGSectionRuleTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _extract(self, capture: PCAPNGWriter): from pcapkit.interface import extract diff --git a/tests/foundation/engines/test_pypcap_engine.py b/tests/foundation/engines/test_pypcap_engine.py index 39e60ccea..f29009c00 100644 --- a/tests/foundation/engines/test_pypcap_engine.py +++ b/tests/foundation/engines/test_pypcap_engine.py @@ -18,7 +18,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -74,7 +74,7 @@ def close(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PyPCAPEngineTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) handle, path = tempfile.mkstemp(suffix='.pcap') os.close(handle) diff --git a/tests/foundation/engines/test_pypcapfile_engine.py b/tests/foundation/engines/test_pypcapfile_engine.py index 77e179f0a..2713eaa2b 100644 --- a/tests/foundation/engines/test_pypcapfile_engine.py +++ b/tests/foundation/engines/test_pypcapfile_engine.py @@ -21,7 +21,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -91,7 +91,7 @@ def __init__(self, packet, layers=0) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PyPCAPFileEngineTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def make_extractor(self, **overrides): sink = OutputSink() diff --git a/tests/foundation/engines/test_runtime_engines.py b/tests/foundation/engines/test_runtime_engines.py index 76602ce1d..6ee6d5583 100644 --- a/tests/foundation/engines/test_runtime_engines.py +++ b/tests/foundation/engines/test_runtime_engines.py @@ -7,7 +7,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper', 'dpkt', 'scapy', 'pyshark') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -29,7 +29,7 @@ def __call__(self, *args, **kwargs): def make_extractor(**overrides): - # imported lazily: setUp purges ``pcapkit`` from sys.modules, so the class + # imported lazily: setUp re-imports ``pcapkit`` for each test class, so the class # has to be fetched from the freshly imported package rather than bound at # module import time from pcapkit.corekit.context import ContextRegistry @@ -80,7 +80,7 @@ def to_dict(self): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PCAPEngineTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_pcap_run_writes_global_header_for_file_and_stream_modes(self) -> None: from pcapkit.const.reg.linktype import LinkType @@ -196,7 +196,7 @@ def __init__(self, file, **kwargs) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class ThirdPartyEngineTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_dpkt_run_magic_selection_get_protocol_and_read_frame(self) -> None: from pcapkit.const.reg.linktype import LinkType diff --git a/tests/foundation/reassembly/data/test_models.py b/tests/foundation/reassembly/data/test_models.py index 61f7d0c08..65ac52967 100644 --- a/tests/foundation/reassembly/data/test_models.py +++ b/tests/foundation/reassembly/data/test_models.py @@ -4,7 +4,7 @@ from ipaddress import ip_address import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -13,7 +13,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class ReassemblyDataModelTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_ip_data_models_and_package_aliases(self) -> None: from pcapkit.const.reg.transtype import TransType diff --git a/tests/foundation/reassembly/test_ip.py b/tests/foundation/reassembly/test_ip.py index 851c6b66d..fd1a2b6a7 100644 --- a/tests/foundation/reassembly/test_ip.py +++ b/tests/foundation/reassembly/test_ip.py @@ -4,7 +4,7 @@ from ipaddress import ip_address import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -13,7 +13,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class IPReassemblyTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _packet(self, *, num: int, fo: int, mf: bool, payload: bytes, header: bytes = b'ip-header', timestamp: float = 1000.0, ident: int = 42): @@ -125,7 +125,7 @@ class IPOverlapConflictTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _packet(self, *, num: int, fo: int, mf: bool, payload: bytes, header: bytes = b'ip-header', timestamp: float = 1000.0, ident: int = 42): @@ -361,7 +361,7 @@ class DeferredAnalysisTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _reassemble(self, *, calls: 'list'): """One complete, unfragmented datagram, and the analyser's call log.""" diff --git a/tests/foundation/reassembly/test_ipv6.py b/tests/foundation/reassembly/test_ipv6.py index 7f666c2d6..d03f0e278 100644 --- a/tests/foundation/reassembly/test_ipv6.py +++ b/tests/foundation/reassembly/test_ipv6.py @@ -5,7 +5,7 @@ import struct import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -72,7 +72,7 @@ class NextHeaderOffsetTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_a_bare_ipv6_header_answers_with_its_own_field(self) -> None: from pcapkit.foundation.reassembly.ipv6 import _next_header_offset @@ -127,7 +127,7 @@ class RectifyHeaderTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def rectify(self, header: bytes) -> bytes: from pcapkit.const.reg.transtype import TransType @@ -183,7 +183,7 @@ class IPv6ReassemblyHeaderTests(unittest.TestCase): """The rewrite reaching the datagram, through the reassembly machinery.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _packet(self, *, num: int, fo: int, mf: bool, payload: bytes, header: bytes): from pcapkit.const.reg.transtype import TransType diff --git a/tests/foundation/reassembly/test_reassembly_base.py b/tests/foundation/reassembly/test_reassembly_base.py index a0eee7536..3807201f7 100644 --- a/tests/foundation/reassembly/test_reassembly_base.py +++ b/tests/foundation/reassembly/test_reassembly_base.py @@ -5,7 +5,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -14,7 +14,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class ReassemblyBaseTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_base_cache_datagram_index_run_and_callbacks(self) -> None: from pcapkit.corekit.infoclass import Info, info_final diff --git a/tests/foundation/reassembly/test_tcp.py b/tests/foundation/reassembly/test_tcp.py index 76c460a51..6d43808ae 100644 --- a/tests/foundation/reassembly/test_tcp.py +++ b/tests/foundation/reassembly/test_tcp.py @@ -5,7 +5,7 @@ import sys import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -21,7 +21,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TCPReassemblyTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _bufid(self): return (ip_address('192.0.2.1'), 12345, ip_address('198.51.100.2'), 443) @@ -277,7 +277,7 @@ class TCPReassemblyCoordinateTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _bufid(self): return (ip_address('192.0.2.1'), 12345, ip_address('198.51.100.2'), 443) @@ -593,7 +593,7 @@ class TCPReassemblyConflictTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _bufid(self): return (ip_address('192.0.2.1'), 12345, ip_address('198.51.100.2'), 443) diff --git a/tests/foundation/reassembly/test_tcp_runtime.py b/tests/foundation/reassembly/test_tcp_runtime.py index d544c43f6..e0be61e61 100644 --- a/tests/foundation/reassembly/test_tcp_runtime.py +++ b/tests/foundation/reassembly/test_tcp_runtime.py @@ -3,7 +3,7 @@ import importlib.util import unittest -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -23,7 +23,7 @@ class TCPReassemblyRuntimeTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_sample_capture_reassembles_every_stream_byte_exactly(self) -> None: """``test.pcap`` through :func:`~pcapkit.interface.extract`. diff --git a/tests/foundation/reassembly/test_timeout.py b/tests/foundation/reassembly/test_timeout.py index 21fbf5658..07780d207 100644 --- a/tests/foundation/reassembly/test_timeout.py +++ b/tests/foundation/reassembly/test_timeout.py @@ -17,7 +17,7 @@ import math import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -29,7 +29,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class IPReassemblyTimeoutTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _packet(self, *, num: int, fo: int, mf: bool, payload: bytes, timestamp: float, id: int = 42): # pylint: disable=redefined-builtin @@ -232,7 +232,7 @@ class IPLooseModeTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _packet(self, *, num: int, fo: int, mf: bool, payload: bytes): from pcapkit.const.reg.transtype import TransType @@ -327,7 +327,7 @@ def test_a_complete_datagram_is_unaffected_by_loose_mode(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TCPReassemblyTimeoutTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _segment(self, *, num: int, seq: int, payload: bytes, timestamp: float, fin: bool = False): diff --git a/tests/foundation/registry/test_foundation.py b/tests/foundation/registry/test_foundation.py index fa8ec9368..f886c7da8 100644 --- a/tests/foundation/registry/test_foundation.py +++ b/tests/foundation/registry/test_foundation.py @@ -4,7 +4,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -13,7 +13,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class FoundationRegistryTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_engine_and_dumper_registration_wrappers(self) -> None: import pcapkit.foundation.registry as registry_pkg @@ -179,7 +179,7 @@ def test_registration_accepts_pcapkit_own_builtin_classes(self) -> None: # path instead of the public wrappers. The subject classes and the read-back # are unchanged. # - # NOTE: imported inside the test because ``setUp`` purges ``pcapkit`` from + # NOTE: imported inside the test because ``setUp`` swaps in a fresh ``pcapkit`` in # ``sys.modules``, which is why every sibling test imports locally too. # ``Extractor`` is needed by name here so the registries can be read back. from pcapkit.foundation.engines.pcap import PCAP as PCAP_Engine diff --git a/tests/foundation/registry/test_foundation_keyword_names.py b/tests/foundation/registry/test_foundation_keyword_names.py index 8659a1ab6..db5694db3 100644 --- a/tests/foundation/registry/test_foundation_keyword_names.py +++ b/tests/foundation/registry/test_foundation_keyword_names.py @@ -19,7 +19,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -28,7 +28,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class RegistryKeywordNameTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_register_extractor_engine_takes_name_not_engine(self) -> None: """``name=`` registers; ``engine=`` -- as documented -- does not.""" diff --git a/tests/foundation/registry/test_protocols.py b/tests/foundation/registry/test_protocols.py index ca120944e..5d0d5e636 100644 --- a/tests/foundation/registry/test_protocols.py +++ b/tests/foundation/registry/test_protocols.py @@ -6,7 +6,7 @@ import warnings from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -37,7 +37,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class ProtocolRegistryTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _unit_protocol(self): from pcapkit.protocols.protocol import ProtocolBase @@ -412,6 +412,7 @@ def test_register_protocol_validates_and_updates_registry(self) -> None: from pcapkit.utilities.exceptions import RegistryError UnitProtocol = self._unit_protocol() + self._guard_registry(registry.protocol_registry, 'UNITPROTOCOL') registry.register_protocol(UnitProtocol) self.assertIs(registry.protocol_registry['UNITPROTOCOL'], UnitProtocol) @@ -457,6 +458,7 @@ def test_top_level_link_internet_and_transport_protocol_wrappers(self) -> None: from pcapkit.utilities.exceptions import ProtocolError, RegistryError UnitProtocol = self._unit_protocol() + self._guard_registry(registry.protocol_registry, 'UNITPROTOCOL') raw_module = ('pcapkit.protocols.misc.raw', 'Raw') class_routes = [ diff --git a/tests/foundation/test_extraction.py b/tests/foundation/test_extraction.py index 47e4fd90e..e06510906 100644 --- a/tests/foundation/test_extraction.py +++ b/tests/foundation/test_extraction.py @@ -13,7 +13,7 @@ import warnings from unittest import mock -from tests._support import purge_modules, sample_path +from tests._support import reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -83,7 +83,20 @@ def close(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class ExtractorTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + + from pcapkit.foundation.extraction import Extractor + + # Several tests here register into the four registries for real -- + # overwriting ``json``, ``dpkt``, ``ipv4`` and ``tcp`` with stand-ins, + # or adding ``unit-*`` names -- so each table is put back in place + # afterwards. A fresh import per test used to do that implicitly; without + # it, a later extraction ran the stand-in engine and hung, and + # :mod:`tests.interface.test_core` found engines it has no constant for + # (GitHub issue #1065). + for table in (Extractor.__output__, Extractor.__engine__, + Extractor.__reassembly__, Extractor.__traceflow__): + self.addCleanup(lambda table=table, saved=dict(table): (table.clear(), table.update(saved))) def _bare_extractor(self): from pcapkit.foundation.extraction import Extractor @@ -1162,7 +1175,7 @@ class DictFrameTraceEndToEndTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @unittest.skipUnless(HAS_DPKT, 'dpkt not installed') def test_dpkt_traced_extraction_writes_flows_instead_of_crashing(self) -> None: diff --git a/tests/foundation/traceflow/data/test_models.py b/tests/foundation/traceflow/data/test_models.py index 0552db397..deaf29601 100644 --- a/tests/foundation/traceflow/data/test_models.py +++ b/tests/foundation/traceflow/data/test_models.py @@ -4,7 +4,7 @@ from ipaddress import ip_address import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -13,7 +13,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TraceFlowDataModelTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_tcp_traceflow_data_models_and_package_aliases(self) -> None: from pcapkit.const.reg.linktype import LinkType diff --git a/tests/foundation/traceflow/test_tcp.py b/tests/foundation/traceflow/test_tcp.py index 696520e9e..1f9448222 100644 --- a/tests/foundation/traceflow/test_tcp.py +++ b/tests/foundation/traceflow/test_tcp.py @@ -6,7 +6,7 @@ import tempfile import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -15,7 +15,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TCPTraceFlowTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _packet(self, *, index: int, src: str = '192.0.2.1', dst: str = '198.51.100.2', srcport: int = 12345, dstport: int = 443, syn: bool = False, diff --git a/tests/foundation/traceflow/test_traceflow_base.py b/tests/foundation/traceflow/test_traceflow_base.py index 5da9a3628..cd114f78b 100644 --- a/tests/foundation/traceflow/test_traceflow_base.py +++ b/tests/foundation/traceflow/test_traceflow_base.py @@ -7,7 +7,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -16,7 +16,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TraceFlowBaseTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_register_dumper_make_fout_and_metadata_fallbacks(self) -> None: from pcapkit.corekit.infoclass import Info, info_final diff --git a/tests/integration/_helpers.py b/tests/integration/_helpers.py index 2686e3bc2..218bbcf6c 100644 --- a/tests/integration/_helpers.py +++ b/tests/integration/_helpers.py @@ -64,7 +64,8 @@ class EndToEndTestCase(unittest.TestCase): def setUpClass(cls) -> None: """Drop the imported library so the class starts from a clean state. - The surrounding tiers purge in :meth:`setUp`, i.e. once per test. A + The surrounding tiers used to purge in :meth:`setUp`, i.e. once per + test, and since GitHub issue #1065 mostly purge once per class too. A fresh :mod:`pcapkit` import measures at roughly 0.7s on this machine, which across this tier would cost more than the extractions themselves, so the purge happens once per class instead. That is equivalent here: diff --git a/tests/integration/test_engine_runtime.py b/tests/integration/test_engine_runtime.py index a2efbad48..e836cddba 100644 --- a/tests/integration/test_engine_runtime.py +++ b/tests/integration/test_engine_runtime.py @@ -6,7 +6,7 @@ import unittest from unittest import mock -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path 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 @@ -17,7 +17,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class EngineRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_default_engine_exposes_native_frame_objects(self) -> None: from pcapkit.interface import extract diff --git a/tests/interface/test_core.py b/tests/interface/test_core.py index 645ead2b6..1abcb2fe8 100644 --- a/tests/interface/test_core.py +++ b/tests/interface/test_core.py @@ -7,7 +7,7 @@ import warnings from unittest import mock -from tests._support import isolate_modules, load_module, purge_modules, sample_path +from tests._support import isolate_modules, load_module, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -220,7 +220,7 @@ class EngineConstantTests(unittest.TestCase): } def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_every_shipped_engine_has_a_constant(self) -> None: # The guard against the next engine landing without one. ``'default'`` is the diff --git a/tests/interface/test_misc.py b/tests/interface/test_misc.py index 722844678..073dee665 100644 --- a/tests/interface/test_misc.py +++ b/tests/interface/test_misc.py @@ -7,7 +7,7 @@ import warnings from unittest import mock -from tests._support import purge_modules, sample_path +from tests._support import reimport_once_per_class, sample_path #: Packages :mod:`pcapkit` needs before it can parse anything at all; the default #: engine and therefore every test here depends on them. They are core install @@ -70,7 +70,8 @@ class FollowTCPStreamTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + # follow_tcp_stream always drives the flow tracer, whose output root # defaults to './tmp' under the working directory when ``fout`` is unset # (TraceFlow.__init__). Point it at a scratch directory so a test run diff --git a/tests/protocols/application/test_application_dispatch_unit.py b/tests/protocols/application/test_application_dispatch_unit.py index 5331a9c1b..a293bdc33 100644 --- a/tests/protocols/application/test_application_dispatch_unit.py +++ b/tests/protocols/application/test_application_dispatch_unit.py @@ -13,7 +13,7 @@ import importlib.util import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -22,7 +22,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class ApplicationDispatchUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @staticmethod def _protocol_class(proto: int | None): diff --git a/tests/protocols/application/test_ftp_unit.py b/tests/protocols/application/test_ftp_unit.py index 8e5341d15..bff5e4a1e 100644 --- a/tests/protocols/application/test_ftp_unit.py +++ b/tests/protocols/application/test_ftp_unit.py @@ -5,7 +5,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -18,7 +18,7 @@ class DummyData(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class FTPUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_ftp_length_is_unsupported(self) -> None: from pcapkit.protocols.application.application import Application diff --git a/tests/protocols/application/test_http_runtime.py b/tests/protocols/application/test_http_runtime.py index 8d9dee6df..fdfe80d36 100644 --- a/tests/protocols/application/test_http_runtime.py +++ b/tests/protocols/application/test_http_runtime.py @@ -3,7 +3,7 @@ import importlib.util import unittest -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -12,7 +12,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class HTTPRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _extract(self, sample: str): from pcapkit.interface import extract diff --git a/tests/protocols/application/test_http_unit.py b/tests/protocols/application/test_http_unit.py index e8d671b63..d377ff1ac 100644 --- a/tests/protocols/application/test_http_unit.py +++ b/tests/protocols/application/test_http_unit.py @@ -6,7 +6,7 @@ from types import SimpleNamespace from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -53,7 +53,7 @@ def http2_frame_bytes(type_: int, flags: int, sid: int, payload: bytes) -> bytes @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class HTTPUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_http_read_rejects_unknown_version_selector(self) -> None: from pcapkit.protocols.application.http import HTTP diff --git a/tests/protocols/application/test_layer_placement_unit.py b/tests/protocols/application/test_layer_placement_unit.py index 151f490b6..7423a8f05 100644 --- a/tests/protocols/application/test_layer_placement_unit.py +++ b/tests/protocols/application/test_layer_placement_unit.py @@ -34,7 +34,7 @@ import tempfile import unittest -from tests._support import purge_modules +from tests._support import purge_modules, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -75,7 +75,7 @@ class ProtocolLayerPlacementTests(unittest.TestCase): """``OSPF``/``RARP`` report ``'Application'`` and still parse and dispatch.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def tearDown(self) -> None: purge_modules(['pcapkit']) diff --git a/tests/protocols/application/test_ospf_unit.py b/tests/protocols/application/test_ospf_unit.py index df0d461df..48831004e 100644 --- a/tests/protocols/application/test_ospf_unit.py +++ b/tests/protocols/application/test_ospf_unit.py @@ -12,7 +12,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import purge_modules, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -25,7 +25,7 @@ class DummyData(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class OSPFUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def tearDown(self) -> None: purge_modules(['pcapkit']) diff --git a/tests/protocols/application/test_rarp_unit.py b/tests/protocols/application/test_rarp_unit.py index 890f5af23..2e538d447 100644 --- a/tests/protocols/application/test_rarp_unit.py +++ b/tests/protocols/application/test_rarp_unit.py @@ -9,7 +9,7 @@ import importlib.util import unittest -from tests._support import purge_modules +from tests._support import purge_modules, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -19,7 +19,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class RARPUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def tearDown(self) -> None: purge_modules(['pcapkit']) diff --git a/tests/protocols/internet/test_ah_unit.py b/tests/protocols/internet/test_ah_unit.py index 42d8f3446..a87e682b9 100644 --- a/tests/protocols/internet/test_ah_unit.py +++ b/tests/protocols/internet/test_ah_unit.py @@ -4,7 +4,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -17,7 +17,7 @@ class DummyDict(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class AHUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_ah_index_length_and_make_data(self) -> None: from pcapkit.const.reg.transtype import TransType diff --git a/tests/protocols/internet/test_esp_unit.py b/tests/protocols/internet/test_esp_unit.py index 9553bd5d5..08fc53e41 100644 --- a/tests/protocols/internet/test_esp_unit.py +++ b/tests/protocols/internet/test_esp_unit.py @@ -30,7 +30,7 @@ import unittest from unittest import mock -from tests._support import close_extractor, purge_modules +from tests._support import close_extractor, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -126,7 +126,7 @@ class ESPRegistryTests(unittest.TestCase): """Algorithm registries and Security Association validation.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_cipher_registry(self) -> None: from pcapkit.const.esp.cipher import Cipher as Const_Cipher @@ -406,7 +406,7 @@ class ESPProtocolTests(unittest.TestCase): """Parsing, decryption and construction.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) ########################################################################## # Registration and identity. diff --git a/tests/protocols/internet/test_hip_locator_set_length_unit.py b/tests/protocols/internet/test_hip_locator_set_length_unit.py index 8c47d09d6..2757965a9 100644 --- a/tests/protocols/internet/test_hip_locator_set_length_unit.py +++ b/tests/protocols/internet/test_hip_locator_set_length_unit.py @@ -76,7 +76,7 @@ import unittest import warnings -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -140,7 +140,7 @@ class HIPLocatorSetLengthTests(unittest.TestCase): """``LOCATOR_SET``'s ``Length`` unit and padding source, over seven shapes.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _make(self, locators: 'list[dict]') -> 'tuple': """Build one ``LOCATOR_SET`` schema and pack it. diff --git a/tests/protocols/internet/test_hip_r1_counter_width_unit.py b/tests/protocols/internet/test_hip_r1_counter_width_unit.py index a18882b33..7e1a632ba 100644 --- a/tests/protocols/internet/test_hip_r1_counter_width_unit.py +++ b/tests/protocols/internet/test_hip_r1_counter_width_unit.py @@ -60,7 +60,7 @@ import importlib.util import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -98,7 +98,7 @@ class HIPR1CounterWidthTests(unittest.TestCase): """``R1_COUNTER``'s counter width, against :rfc:`7401#section-5.2.3`.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_r1_counter_record_is_the_rfc_total_at_both_codes(self) -> None: """Both codes pack the 16 octets ``Length = 12`` requires. diff --git a/tests/protocols/internet/test_hip_unit.py b/tests/protocols/internet/test_hip_unit.py index ad26eb3cb..af98502ff 100644 --- a/tests/protocols/internet/test_hip_unit.py +++ b/tests/protocols/internet/test_hip_unit.py @@ -7,7 +7,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -20,7 +20,7 @@ class DummyDict(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class HIPUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_hip_index_length_alias_and_make_data(self) -> None: from pcapkit.const.hip.packet import Packet diff --git a/tests/protocols/internet/test_internet_unit.py b/tests/protocols/internet/test_internet_unit.py index c51e2fe02..525c08d18 100644 --- a/tests/protocols/internet/test_internet_unit.py +++ b/tests/protocols/internet/test_internet_unit.py @@ -6,7 +6,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -20,7 +20,7 @@ class MutableInfo(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class InternetBaseUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _make_internet_class(self, default_protocol: type | None = None): from pcapkit.protocols.internet.internet import Internet diff --git a/tests/protocols/internet/test_ip_runtime.py b/tests/protocols/internet/test_ip_runtime.py index c7747fe79..4bfa98b06 100644 --- a/tests/protocols/internet/test_ip_runtime.py +++ b/tests/protocols/internet/test_ip_runtime.py @@ -3,7 +3,7 @@ import importlib.util import unittest -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -12,7 +12,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class InternetProtocolRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _extract(self, sample: str): from pcapkit.interface import extract diff --git a/tests/protocols/internet/test_ipv4_unit.py b/tests/protocols/internet/test_ipv4_unit.py index cdfbd38c8..a4c88d1d4 100644 --- a/tests/protocols/internet/test_ipv4_unit.py +++ b/tests/protocols/internet/test_ipv4_unit.py @@ -9,7 +9,7 @@ from unittest import mock import warnings -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -18,7 +18,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class IPv4UnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_ipv4_index_returns_expected_registry_value(self) -> None: from pcapkit.const.reg.transtype import TransType diff --git a/tests/protocols/internet/test_ipv6_ext_unit.py b/tests/protocols/internet/test_ipv6_ext_unit.py index cfb28cba3..a9524465c 100644 --- a/tests/protocols/internet/test_ipv6_ext_unit.py +++ b/tests/protocols/internet/test_ipv6_ext_unit.py @@ -6,7 +6,7 @@ import unittest import warnings -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -39,7 +39,7 @@ class IPv6ExtUnitTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) # -- direct construction: the per-protocol length rules ----------------- @@ -120,6 +120,11 @@ def test_overrun_warns_and_stops_instead_of_clipping(self) -> None: # -- __index__ and the guarded extension-mode accessors ------------------ def test_index_raises_because_no_class_level_identity_exists(self) -> None: + """The fallback has no class-level identity, so it keeps raising rather + than being handed a placeholder index. This is also the other half of + the #917 contract in :class:`IPv6ExtSharedBaseContractTests`, which + relies on this test rather than repeating it. + """ from pcapkit.protocols.internet.ipv6_ext import IPv6_Ext from pcapkit.utilities.exceptions import UnsupportedCall @@ -420,7 +425,7 @@ class IPv6ExtSharedBaseContractTests(unittest.TestCase): }) def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) @staticmethod def _subclasses(base: type) -> 'list[type]': @@ -612,17 +617,6 @@ def test_no_subclass_answers_with_the_fallback_identity(self) -> None: self.assertNotEqual(self._constant(klass, 'name'), base_name) self.assertNotEqual(self._constant(klass, 'alias'), base_alias) - def test_the_base_itself_still_has_no_class_level_identity(self) -> None: - """The other half of the contract: the fallback genuinely has none, so - it must keep raising rather than be handed a placeholder index to - satisfy the rule above. - """ - from pcapkit.protocols.internet.ipv6_ext import IPv6_Ext - from pcapkit.utilities.exceptions import UnsupportedCall - - with self.assertRaises(UnsupportedCall): - IPv6_Ext.__index__() - def test_ah_and_esp_double_inherit_and_linearise(self) -> None: """Both are IPsec members *and* IPv6 extension headers -- IANA marks each ``Y`` in the *IPv6 Extension Header* column -- so each carries two diff --git a/tests/protocols/internet/test_ipv6_extension_runtime.py b/tests/protocols/internet/test_ipv6_extension_runtime.py index 8881bd706..309267098 100644 --- a/tests/protocols/internet/test_ipv6_extension_runtime.py +++ b/tests/protocols/internet/test_ipv6_extension_runtime.py @@ -3,7 +3,7 @@ import importlib.util import unittest -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -62,7 +62,7 @@ def build_fragment_header(next_header: int) -> bytes: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class IPv6ExtensionRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _extract(self, sample: str): from pcapkit.interface import extract @@ -145,7 +145,7 @@ class IPv6ExtensionPayloadOffsetTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _parse(self, next_header: int, extensions: bytes): from pcapkit.protocols.internet.ipv6 import IPv6 diff --git a/tests/protocols/internet/test_ipv6_extension_unit.py b/tests/protocols/internet/test_ipv6_extension_unit.py index d5cabe1e5..1779a31fc 100644 --- a/tests/protocols/internet/test_ipv6_extension_unit.py +++ b/tests/protocols/internet/test_ipv6_extension_unit.py @@ -8,7 +8,7 @@ import unittest from unittest import mock -from tests._support import purge_modules, time_limit +from tests._support import reimport_once_per_class, time_limit RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -21,7 +21,7 @@ class DummyDict(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class IPv6ExtensionUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _assert_extension_accessors_blocked(self, protocol_cls: type) -> None: from pcapkit.utilities.exceptions import UnsupportedCall diff --git a/tests/protocols/internet/test_ipv6_reassembly_runtime.py b/tests/protocols/internet/test_ipv6_reassembly_runtime.py index 2dc7e94c3..f2796ad94 100644 --- a/tests/protocols/internet/test_ipv6_reassembly_runtime.py +++ b/tests/protocols/internet/test_ipv6_reassembly_runtime.py @@ -6,7 +6,7 @@ import tempfile import unittest -from tests._support import close_extractor, purge_modules +from tests._support import close_extractor, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -68,7 +68,8 @@ class IPv6ReassemblyRuntimeTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + self._tmpdir = tempfile.TemporaryDirectory() self.addCleanup(self._tmpdir.cleanup) self.tmp_path = pathlib.Path(self._tmpdir.name) diff --git a/tests/protocols/internet/test_ipv6_unit.py b/tests/protocols/internet/test_ipv6_unit.py index 4867c3bd3..264d49024 100644 --- a/tests/protocols/internet/test_ipv6_unit.py +++ b/tests/protocols/internet/test_ipv6_unit.py @@ -5,7 +5,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -18,7 +18,7 @@ class DummyDict(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class IPv6UnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_ipv6_index_length_and_id_are_stable(self) -> None: from pcapkit.const.reg.transtype import TransType diff --git a/tests/protocols/internet/test_ipx_unit.py b/tests/protocols/internet/test_ipx_unit.py index ee2c5e501..86dfdf251 100644 --- a/tests/protocols/internet/test_ipx_unit.py +++ b/tests/protocols/internet/test_ipx_unit.py @@ -4,7 +4,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -17,7 +17,7 @@ class DummyDict(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class IPXUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_ipx_index_length_and_make_data(self) -> None: from pcapkit.const.reg.transtype import TransType diff --git a/tests/protocols/internet/test_mh_unit.py b/tests/protocols/internet/test_mh_unit.py index a208ea0a9..51a1b82e1 100644 --- a/tests/protocols/internet/test_mh_unit.py +++ b/tests/protocols/internet/test_mh_unit.py @@ -8,7 +8,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -21,7 +21,7 @@ class DummyDict(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class MHUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_mh_index_length_and_make_data(self) -> None: from pcapkit.const.mh.packet import Packet diff --git a/tests/protocols/link/test_l2tp_version_unit.py b/tests/protocols/link/test_l2tp_version_unit.py index 965e54a29..c5a7078f2 100644 --- a/tests/protocols/link/test_l2tp_version_unit.py +++ b/tests/protocols/link/test_l2tp_version_unit.py @@ -40,7 +40,7 @@ class can read, and no such class exists in this tree. import tempfile import unittest -from tests._support import close_extractor, purge_modules +from tests._support import close_extractor, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -101,7 +101,7 @@ class L2TPVersionTests(unittest.TestCase): """:class:`L2TPv2` parses version 2 and refuses everything else.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def parse(self, data: bytes): """Parse ``data`` as :class:`L2TPv2` directly, with no dispatch.""" diff --git a/tests/protocols/link/test_link_runtime.py b/tests/protocols/link/test_link_runtime.py index 7a09a5704..3cc346b0f 100644 --- a/tests/protocols/link/test_link_runtime.py +++ b/tests/protocols/link/test_link_runtime.py @@ -3,7 +3,7 @@ import importlib.util import unittest -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -12,7 +12,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class LinkProtocolRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _extract(self, sample: str): from pcapkit.interface import extract diff --git a/tests/protocols/link/test_link_unit.py b/tests/protocols/link/test_link_unit.py index f353a1dc9..f28215edf 100644 --- a/tests/protocols/link/test_link_unit.py +++ b/tests/protocols/link/test_link_unit.py @@ -6,7 +6,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -19,7 +19,7 @@ class DummyData(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class LinkProtocolUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_vlan_indices_follow_the_one_module_per_index_rule(self) -> None: """Each tag declares the EtherType it is reached by; the base declares none. diff --git a/tests/protocols/misc/pcap/test_frame_endian_runtime.py b/tests/protocols/misc/pcap/test_frame_endian_runtime.py index bb3df1d1d..73a4d969e 100644 --- a/tests/protocols/misc/pcap/test_frame_endian_runtime.py +++ b/tests/protocols/misc/pcap/test_frame_endian_runtime.py @@ -47,7 +47,7 @@ import unittest from decimal import Decimal -from tests._support import purge_modules, sample_path +from tests._support import reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -107,7 +107,7 @@ class PcapFrameByteOrderRuntimeTests(unittest.TestCase): """#605, through :func:`pcapkit.interface.extract`.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def read(self, name: 'str') -> 'tuple[bytes, list]': """Read a fixture's octets and its frames. diff --git a/tests/protocols/misc/pcap/test_frame_length_runtime.py b/tests/protocols/misc/pcap/test_frame_length_runtime.py index 8b62a44f2..6b0fe8727 100644 --- a/tests/protocols/misc/pcap/test_frame_length_runtime.py +++ b/tests/protocols/misc/pcap/test_frame_length_runtime.py @@ -61,7 +61,7 @@ import struct import unittest -from tests._support import purge_modules, sample_path +from tests._support import reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -178,7 +178,7 @@ class PcapFrameLengthRuntimeTests(unittest.TestCase): """#618, on the classic-PCAP reader -- the one that was wrong.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def frames(self, name: 'str') -> 'tuple[bytes, list]': """Read a fixture's octets and the frames :mod:`pcapkit` parses from it. @@ -290,7 +290,7 @@ class PcapngFrameLengthRuntimeTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def blocks(self) -> 'tuple[bytes, list]': """Read ``test.pcapng``'s octets and the packet blocks parsed from it. @@ -378,7 +378,7 @@ class ReadersAgreeRuntimeTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_a_truncated_frame_reads_alike_whichever_container_it_came_from(self) -> None: """One caller, two capture formats, one meaning per attribute. diff --git a/tests/protocols/misc/pcap/test_frame_runtime.py b/tests/protocols/misc/pcap/test_frame_runtime.py index 52c11775f..6a435451b 100644 --- a/tests/protocols/misc/pcap/test_frame_runtime.py +++ b/tests/protocols/misc/pcap/test_frame_runtime.py @@ -4,7 +4,7 @@ import struct import unittest -from tests._support import purge_modules, sample_path +from tests._support import reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -13,7 +13,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PcapFrameRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_frame_exposes_expected_public_metadata(self) -> None: from pcapkit.interface import extract diff --git a/tests/protocols/misc/pcap/test_header_frame_unit.py b/tests/protocols/misc/pcap/test_header_frame_unit.py index 68046eaa9..e8c267cd1 100644 --- a/tests/protocols/misc/pcap/test_header_frame_unit.py +++ b/tests/protocols/misc/pcap/test_header_frame_unit.py @@ -9,7 +9,7 @@ from types import SimpleNamespace from unittest import mock -from tests._support import purge_modules, sample_path +from tests._support import reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -25,7 +25,7 @@ def __update__(self, values): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PCAPHeaderFrameUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_header_make_length_index_and_make_data(self) -> None: from pcapkit.const.reg.linktype import LinkType diff --git a/tests/protocols/misc/test_pcapng_unit.py b/tests/protocols/misc/test_pcapng_unit.py index 9bd8fca62..1930b8b58 100644 --- a/tests/protocols/misc/test_pcapng_unit.py +++ b/tests/protocols/misc/test_pcapng_unit.py @@ -20,7 +20,7 @@ import warnings from unittest import mock -from tests._support import ROOT, purge_modules, sample_path +from tests._support import ROOT, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -62,7 +62,7 @@ def block_body(body: bytes) -> bytes: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PCAPNGUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_pcapng_index_length_and_make_data(self) -> None: from pcapkit.const.pcapng.block_type import BlockType @@ -1487,8 +1487,15 @@ class LocalIterableOption(schema_pcapng.Option, self.assertIs(registry['opt'][OptionType.opt_comment], LocalDefaultOption) self.assertIs(registry['localtest'][OptionType.if_name], LocalIterableOption) finally: - registry.clear() - registry.update(saved_registry) + # Restored in place: the per-namespace tables are shared by + # reference, so swapping in copies would leave the originals -- + # still holding ``LocalDefaultOption`` -- live for every later + # test in the same import (GitHub issue #1065). + for key in [key for key in registry if key not in saved_registry]: + del registry[key] + for key, saved in saved_registry.items(): + registry[key].clear() + registry[key].update(saved) mismatch = schema_pcapng.UnknownBlock(length=16, body=b'abcd', length2=20) with mock.patch('pcapkit.protocols.schema.misc.pcapng.warn') as warn: @@ -3483,7 +3490,7 @@ class PCAPNGOptionAreaBoundTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _unpack_epb(self, options: bytes, packet_data: bytes = bytes(4)): """Unpack an EPB whose option area is exactly ``options``.""" @@ -3901,8 +3908,19 @@ class PCAPNGTruncatedFileTests(unittest.TestCase): """ + #: The result of :meth:`_sweep`, computed by the first test that asks for it + #: and shared by the other three: one extraction per octet of the sample is + #: the cost of this class, and every test reads the same result (GitHub + #: issue #1062). + _swept = None # type: tuple[dict[int, int], dict[int, BaseException]] | None + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls._swept = None + def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) with open(sample_path('dhcp.pcapng'), 'rb') as stream: self.whole = stream.read() @@ -3925,7 +3943,17 @@ def _sweep(self) -> 'tuple[dict[int, int], dict[int, BaseException]]': have chosen by hand -- 372 and 373 are where ``struct.error`` lived, and 376 is where the cut happens to land on a block boundary. + Computed once per class, lazily rather than in :meth:`setUpClass`, so the + sweep runs against the class's own :mod:`pcapkit` import, which + :func:`tests._support.reimport_once_per_class` sets up from ``setUp``. + """ + if type(self)._swept is None: + type(self)._swept = self._sweep_uncached() + return type(self)._swept + + def _sweep_uncached(self) -> 'tuple[dict[int, int], dict[int, BaseException]]': + """Run the sweep :meth:`_sweep` caches.""" frames = {} # type: dict[int, int] failures = {} # type: dict[int, BaseException] @@ -4177,7 +4205,7 @@ class PCAPNGNegativeLengthTests(unittest.TestCase): } def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _hostile_packet(self): """``HOSTILE``, with zero for any key it does not name.""" @@ -4895,7 +4923,7 @@ class PCAPNGOptionRegistryGuardTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _snapshot(self): """Restore every namespace dictionary on teardown.""" diff --git a/tests/protocols/misc/test_raw_null_unit.py b/tests/protocols/misc/test_raw_null_unit.py index 86ad299a8..b24c12353 100644 --- a/tests/protocols/misc/test_raw_null_unit.py +++ b/tests/protocols/misc/test_raw_null_unit.py @@ -4,7 +4,7 @@ import unittest from types import SimpleNamespace -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -17,7 +17,7 @@ class DummyData(dict): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class RawNullUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_raw_properties_make_data_and_index_error(self) -> None: from pcapkit.protocols.misc.raw import Raw diff --git a/tests/protocols/schema/test_enum_schema_registry_unit.py b/tests/protocols/schema/test_enum_schema_registry_unit.py index 2f450fa9c..d5905229f 100644 --- a/tests/protocols/schema/test_enum_schema_registry_unit.py +++ b/tests/protocols/schema/test_enum_schema_registry_unit.py @@ -21,7 +21,7 @@ import unittest import warnings -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -30,7 +30,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class EnumSchemaRegistryRetentionTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_lookup_miss_on_auto_created_registry_does_not_grow_it(self) -> None: """A miss on an auto-created ``__enum__`` must not be retained. @@ -186,7 +186,7 @@ class EnumSchemaRegistryOverwriteTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _guard_registry(self, registry, key) -> None: """Restore ``key`` in ``registry`` on teardown, absence included. 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 f7ae3c15c..04dd3a463 100644 --- a/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py +++ b/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py @@ -5,7 +5,7 @@ from collections import ChainMap from collections.abc import Mapping -from tests._support import purge_modules +from tests._support import purge_modules, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -56,7 +56,7 @@ def setUp(self) -> None: # both matter: a stale positive cache entry left over from an earlier # test in the same process would make the very first assertion below # true for the wrong reason. - purge_modules(['pcapkit']) + reimport_once_per_class(self) def tearDown(self) -> None: # A couple of tests below deliberately poison collections.abc's shared @@ -307,7 +307,7 @@ class SchemaMetaReservedClassKwargsTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_each_reserved_keyword_raises_a_named_schema_error(self) -> None: """Every reserved keyword raises :exc:`SchemaError`, not a bare TypeError. diff --git a/tests/protocols/schema/test_schema_unit.py b/tests/protocols/schema/test_schema_unit.py index 29935729b..408f13328 100644 --- a/tests/protocols/schema/test_schema_unit.py +++ b/tests/protocols/schema/test_schema_unit.py @@ -8,7 +8,7 @@ import warnings from unittest import mock -from tests._support import purge_modules, time_limit +from tests._support import reimport_once_per_class, time_limit RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -17,7 +17,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class SchemaUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _make_schema_classes(self): from pcapkit.corekit.fields.collections import ListField diff --git a/tests/protocols/test_dispatch_bindings_unit.py b/tests/protocols/test_dispatch_bindings_unit.py index 044e43af5..e8662f917 100644 --- a/tests/protocols/test_dispatch_bindings_unit.py +++ b/tests/protocols/test_dispatch_bindings_unit.py @@ -19,7 +19,7 @@ import tempfile import unittest -from tests._support import close_extractor, purge_modules +from tests._support import close_extractor, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -96,7 +96,7 @@ class DispatchBindingTests(unittest.TestCase): """Each registered code reaches the dissector the table names.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def extract(self, *frames: bytes): """Extract synthesised ``frames`` and return the frame list.""" diff --git a/tests/protocols/test_dispatch_default_resolution_unit.py b/tests/protocols/test_dispatch_default_resolution_unit.py index 9f5d169ca..c5938037b 100644 --- a/tests/protocols/test_dispatch_default_resolution_unit.py +++ b/tests/protocols/test_dispatch_default_resolution_unit.py @@ -49,7 +49,7 @@ import warnings from unittest import mock -from tests._support import purge_modules +from tests._support import purge_modules, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -61,7 +61,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class DefaultDescriptorResolutionTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def tearDown(self) -> None: # ``test_no_stale_class_survives_a_module_reload`` leaves a reloaded diff --git a/tests/protocols/test_dispatch_reachability_unit.py b/tests/protocols/test_dispatch_reachability_unit.py index b9b4bc912..a52bee211 100644 --- a/tests/protocols/test_dispatch_reachability_unit.py +++ b/tests/protocols/test_dispatch_reachability_unit.py @@ -30,12 +30,13 @@ """ from __future__ import annotations +import gc import importlib.util import pkgutil import unittest from typing import TYPE_CHECKING, NamedTuple -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -144,7 +145,7 @@ class DispatchReachabilityTests(unittest.TestCase): """No protocol class declares a code that nothing dispatches.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_every_indexed_class_is_reachable_under_the_code_it_declares(self) -> None: """The invariant itself, over every class rather than a hand-picked few.""" @@ -191,8 +192,12 @@ def __index__(cls) -> 'Any': f'audit missed the injected gap; reported {names}') finally: # ``__subclasses__`` holds a weak reference, but the class is only - # collected once nothing in this frame names it. + # collected once nothing in this frame names it -- and, a class + # being a reference cycle of its own, once the cycle collector has + # run. Until then the next audit in the same import still sees it + # (GitHub issue #1065: no longer one import per test). del Unreachable + gc.collect() def test_transtype_l2tp_is_unbound_because_v3_has_no_class(self) -> None: """115's gap is a missing *class*, not a missing registration. diff --git a/tests/protocols/test_generated_pcap_runtime.py b/tests/protocols/test_generated_pcap_runtime.py index d4f4cb163..c8bd7f97b 100644 --- a/tests/protocols/test_generated_pcap_runtime.py +++ b/tests/protocols/test_generated_pcap_runtime.py @@ -5,7 +5,7 @@ import unittest from pathlib import Path -from tests._support import close_extractor, purge_modules +from tests._support import close_extractor, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -15,7 +15,7 @@ @unittest.skipUnless(HAS_RUNTIME and HAS_SCAPY, 'runtime or scapy dependencies not installed') class GeneratedPcapRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_scapy_generated_pcap_decodes_ipv4_udp_and_ipv6_tcp(self) -> None: from scapy.all import Ether, IP, IPv6, Raw, TCP, UDP, wrpcap diff --git a/tests/protocols/test_option_coverage_runtime.py b/tests/protocols/test_option_coverage_runtime.py index 13575143a..42a41a5c4 100644 --- a/tests/protocols/test_option_coverage_runtime.py +++ b/tests/protocols/test_option_coverage_runtime.py @@ -35,7 +35,7 @@ import unittest from typing import TYPE_CHECKING -from tests._support import close_extractor, purge_modules, time_limit +from tests._support import close_extractor, reimport_once_per_class, time_limit from tests._tiers import SAMPLE_ROOT if TYPE_CHECKING: @@ -86,7 +86,7 @@ class OptionCoverageCaptureTests(unittest.TestCase): """The generated option captures extract cleanly.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_every_option_capture_extracts(self) -> None: """Each capture reads back, with every frame reaching its expected layer. diff --git a/tests/protocols/test_pcapng_regression.py b/tests/protocols/test_pcapng_regression.py index 17a855c7f..aa3447fbc 100644 --- a/tests/protocols/test_pcapng_regression.py +++ b/tests/protocols/test_pcapng_regression.py @@ -7,7 +7,7 @@ import unittest import warnings -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -34,7 +34,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PcapngRegressionTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_dhcp_pcapng_extracts_successfully(self) -> None: from pcapkit.interface import extract @@ -209,7 +209,7 @@ class PcapngPayloadOctetsTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _extract(self, path: str) -> 'object': from pcapkit.interface import extract diff --git a/tests/protocols/test_protocol_base_unit.py b/tests/protocols/test_protocol_base_unit.py index bc91c046e..30d2e99fb 100644 --- a/tests/protocols/test_protocol_base_unit.py +++ b/tests/protocols/test_protocol_base_unit.py @@ -8,7 +8,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -17,7 +17,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class ProtocolBaseUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _make_protocol_class(self): from pcapkit.corekit.infoclass import info_final diff --git a/tests/protocols/test_protocol_code_registration_unit.py b/tests/protocols/test_protocol_code_registration_unit.py index f174dda50..a3050960d 100644 --- a/tests/protocols/test_protocol_code_registration_unit.py +++ b/tests/protocols/test_protocol_code_registration_unit.py @@ -32,7 +32,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -43,7 +43,7 @@ class ProtocolCodeOptInTests(unittest.TestCase): """The opt-in gate itself: ``register_protocol_code`` runs iff ``code`` is given.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_code_omitted_leaves_next_layer_dispatch_unregistered(self) -> None: """No ``code=`` -> :func:`register_protocol_code` is never called.""" @@ -146,7 +146,7 @@ class RegisterProtocolCodeInferenceTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_bare_ethertype_member_infers_link(self) -> None: from pcapkit.const.reg.ethertype import EtherType @@ -338,7 +338,7 @@ class ProtocolCodeEndToEndTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_end_to_end_class_statement_registers_the_real_table(self) -> None: from pcapkit.const.reg.transtype import TransType diff --git a/tests/protocols/test_registry_runtime.py b/tests/protocols/test_registry_runtime.py index 1f6eca291..c0a61e941 100644 --- a/tests/protocols/test_registry_runtime.py +++ b/tests/protocols/test_registry_runtime.py @@ -4,7 +4,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -13,7 +13,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class ProtocolRegistryRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_link_register_rejects_non_protocol_types(self) -> None: from pcapkit.const.reg.ethertype import EtherType diff --git a/tests/protocols/transport/test_tcp_http_dispatch_unit.py b/tests/protocols/transport/test_tcp_http_dispatch_unit.py index 8fbdbc643..23b372a2a 100644 --- a/tests/protocols/transport/test_tcp_http_dispatch_unit.py +++ b/tests/protocols/transport/test_tcp_http_dispatch_unit.py @@ -112,7 +112,7 @@ def test_http2_on_the_http_ports_decodes_as_http2_and_http1_still_as_http1(self) # is entitled to complain about; the chain is what is under # test. warnings.simplefilter('ignore') - with time_limit(5): + with time_limit(): proto = TCP(raw, len(raw)) self.assertEqual(str(proto.protochain), expected) diff --git a/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py b/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py index 48eac11d8..478542a5b 100644 --- a/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py @@ -45,7 +45,7 @@ import importlib.util -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -118,7 +118,7 @@ def build_mptcp(*, syn: 'bool' = False, ack: 'bool' = False, **option: 'Any') -> @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TCPMPTCPErrorMessageUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_read_mptcp_join_message(self) -> None: """``_read_mptcp_join``'s fall-through, the message #649 was reported against. diff --git a/tests/protocols/transport/test_tcp_runtime.py b/tests/protocols/transport/test_tcp_runtime.py index bd24878a5..df806466a 100644 --- a/tests/protocols/transport/test_tcp_runtime.py +++ b/tests/protocols/transport/test_tcp_runtime.py @@ -3,7 +3,7 @@ import importlib.util import unittest -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -12,7 +12,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TCPRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _extract(self, sample: str): from pcapkit.interface import extract diff --git a/tests/protocols/transport/test_tcp_udp_unit.py b/tests/protocols/transport/test_tcp_udp_unit.py index 99771e1b0..2d88f5517 100644 --- a/tests/protocols/transport/test_tcp_udp_unit.py +++ b/tests/protocols/transport/test_tcp_udp_unit.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -69,7 +69,7 @@ def mptcp_option(opt: 'Any' = None, *, syn: 'bool' = False, ack: 'bool' = False, @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TCPUDPUnitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_udp_index_make_and_make_data_preserve_core_fields(self) -> None: from pcapkit.const.reg.apptype import AppType, TransportProtocol diff --git a/tests/protocols/transport/test_udp_runtime.py b/tests/protocols/transport/test_udp_runtime.py index 3e427c37d..62e552bc0 100644 --- a/tests/protocols/transport/test_udp_runtime.py +++ b/tests/protocols/transport/test_udp_runtime.py @@ -3,7 +3,7 @@ import importlib.util import unittest -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -12,7 +12,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class UDPRuntimeTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _extract(self, sample: str, *, store: bool = True, auto: bool = True): from pcapkit.interface import extract diff --git a/tests/toolkit/test_dpkt_unit.py b/tests/toolkit/test_dpkt_unit.py index d03fe5d36..7ee8de231 100644 --- a/tests/toolkit/test_dpkt_unit.py +++ b/tests/toolkit/test_dpkt_unit.py @@ -10,7 +10,7 @@ import unittest import warnings -from tests._support import close_extractor, purge_modules, sample_path +from tests._support import close_extractor, reimport_once_per_class, sample_path HAS_DPKT = importlib.util.find_spec('dpkt') is not None RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') @@ -117,7 +117,7 @@ def pack(self) -> bytes: @unittest.skipUnless(HAS_RUNTIME and HAS_DPKT, 'runtime dependencies not installed') class DPKTToolkitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _make_ipv4_tcp_packet(self, *, df: bool = False, fragmented: bool = False): import dpkt @@ -394,7 +394,7 @@ class DPKTTCPHeaderSplitTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_header_ends_at_data_offset_and_len_matches_payload(self) -> None: from pcapkit.toolkit import dpkt as toolkit @@ -489,7 +489,7 @@ class DPKTIPv4ReassemblyFieldTests(unittest.TestCase): """The IPv4 path carries the same class of defect as the TCP one.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_header_ends_at_internet_header_length_with_options(self) -> None: from pcapkit.toolkit import dpkt as toolkit @@ -576,7 +576,7 @@ class DPKTIPv6ReassemblyTests(unittest.TestCase): """IPv6 reassembly against a real :class:`dpkt.ip6.IP6FragmentHeader`.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_reads_the_real_fragment_header_attributes(self) -> None: import dpkt @@ -790,7 +790,7 @@ class DPKTEngineParityTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _reassemble(self, path: str, engine: str): import pcapkit @@ -840,7 +840,7 @@ class DPKTTimestampTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_the_engine_attaches_each_frames_capture_timestamp(self) -> None: import pcapkit diff --git a/tests/toolkit/test_pcap_ct_unit.py b/tests/toolkit/test_pcap_ct_unit.py index b0f30f9c0..cc8226e56 100644 --- a/tests/toolkit/test_pcap_ct_unit.py +++ b/tests/toolkit/test_pcap_ct_unit.py @@ -18,7 +18,7 @@ import importlib.util import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -27,7 +27,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PCAP_CTToolkitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_packet2chain_reports_the_link_layer_and_raw(self) -> None: from pcapkit.const.reg.linktype import LinkType diff --git a/tests/toolkit/test_pcap_unit.py b/tests/toolkit/test_pcap_unit.py index 28038f60a..e99126092 100644 --- a/tests/toolkit/test_pcap_unit.py +++ b/tests/toolkit/test_pcap_unit.py @@ -5,7 +5,7 @@ import types import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -39,7 +39,7 @@ def port(value: int) -> types.SimpleNamespace: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PCAPToolkitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _make_ipv4(self, *, df: bool = False): from pcapkit.const.reg.transtype import TransType diff --git a/tests/toolkit/test_pypcap_unit.py b/tests/toolkit/test_pypcap_unit.py index ecde79236..67b953e7e 100644 --- a/tests/toolkit/test_pypcap_unit.py +++ b/tests/toolkit/test_pypcap_unit.py @@ -13,7 +13,7 @@ import importlib.util import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -22,7 +22,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PyPCAPToolkitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_packet2chain_reports_the_link_layer_and_raw(self) -> None: from pcapkit.const.reg.linktype import LinkType diff --git a/tests/toolkit/test_pypcapfile_unit.py b/tests/toolkit/test_pypcapfile_unit.py index 2c995dad0..1c70fe7f4 100644 --- a/tests/toolkit/test_pypcapfile_unit.py +++ b/tests/toolkit/test_pypcapfile_unit.py @@ -20,7 +20,7 @@ import types import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -138,7 +138,7 @@ def make_packet(layer, *, timestamp: int = 1511106545, timestamp_us: int = 47171 @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PyPCAPFileToolkitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) ########################################################################## # Auxiliary functions. @@ -388,7 +388,7 @@ class PyPCAPFileToolkitAgainstRealDecodersTests(unittest.TestCase): """Tests that need :mod:`pcapfile`'s own decoders to be meaningful.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_ipv4_header_reconstruction_is_byte_exact(self) -> None: from pcapfile.protocols.network.ip import IP diff --git a/tests/toolkit/test_pyshark_unit.py b/tests/toolkit/test_pyshark_unit.py index ff7597d41..fc7b82ea5 100644 --- a/tests/toolkit/test_pyshark_unit.py +++ b/tests/toolkit/test_pyshark_unit.py @@ -3,7 +3,7 @@ import importlib.util import unittest -from tests._support import purge_modules +from tests._support import reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -88,7 +88,7 @@ def __contains__(self, name: str) -> bool: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PySharkToolkitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def test_packet2dict_nests_frame_and_layers(self) -> None: from pcapkit.toolkit import pyshark as toolkit diff --git a/tests/toolkit/test_scapy_unit.py b/tests/toolkit/test_scapy_unit.py index a82a35db7..a82a2d35d 100644 --- a/tests/toolkit/test_scapy_unit.py +++ b/tests/toolkit/test_scapy_unit.py @@ -7,7 +7,7 @@ import unittest from unittest import mock -from tests._support import purge_modules +from tests._support import purge_modules, reimport_once_per_class HAS_SCAPY = importlib.util.find_spec('scapy') is not None RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') @@ -17,7 +17,7 @@ @unittest.skipUnless(HAS_RUNTIME and HAS_SCAPY, 'runtime dependencies not installed') class ScapyToolkitTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) def _ether_kwargs(self) -> dict[str, str]: return { @@ -103,6 +103,12 @@ def _make_ip_rooted_tcp_packet(self): return IP(bytes(packet)) def test_import_without_scapy_sets_none_and_warns(self) -> None: + # Put a real import back afterwards, for the tests that share this + # class's import (GitHub issue #1065). Cleanups run last-in first-out: + # purge the scapy-less copy, then re-import, which also rebinds the + # ``pcapkit.toolkit.scapy`` attribute a ``from`` import reads first. + self.addCleanup(importlib.import_module, 'pcapkit.toolkit.scapy') + self.addCleanup(purge_modules, ['pcapkit.toolkit.scapy']) purge_modules(['pcapkit.toolkit.scapy']) real_import = builtins.__import__ diff --git a/tests/utilities/test_compat.py b/tests/utilities/test_compat.py index 55b190e3d..d434085ff 100644 --- a/tests/utilities/test_compat.py +++ b/tests/utilities/test_compat.py @@ -57,7 +57,7 @@ import unittest from unittest import mock -from tests._support import load_module, purge_modules +from tests._support import load_module, purge_modules, reimport_once_per_class # NOTE: :mod:`aenum` is deliberately *not* imported at module scope, however # convenient that would be for the assertions below. A module-level import would @@ -170,6 +170,8 @@ def aenum_pyver_readings() -> 'dict[str, tuple[int, ...]]': class CompatTests(unittest.TestCase): def setUp(self) -> None: + reimport_once_per_class(self) + # Registered before anything else, and deliberately first: cleanups run # last-in-first-out, so this one runs *after* the module-table restore # that ``load_module`` arranges below and after the test body however it @@ -179,7 +181,6 @@ def setUp(self) -> None: # fake a version without going through ``load_compat_as_python35``. self.addCleanup(self.assert_aenum_is_not_poisoned) - purge_modules(['pcapkit']) self.compat = load_module('pcapkit.utilities.compat', 'pcapkit/utilities/compat.py') def assert_aenum_is_not_poisoned(self) -> None: diff --git a/tests/utilities/test_exceptions_warnings.py b/tests/utilities/test_exceptions_warnings.py index 2585d7b54..9bcc5af68 100644 --- a/tests/utilities/test_exceptions_warnings.py +++ b/tests/utilities/test_exceptions_warnings.py @@ -5,12 +5,13 @@ import warnings as pywarnings from unittest import mock -from tests._support import bootstrap_core_modules, purge_modules +from tests._support import bootstrap_core_modules, reimport_once_per_class class ExceptionsWarningsTests(unittest.TestCase): def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + modules = bootstrap_core_modules() self.exceptions = modules['exceptions'] self.warnings = modules['warnings'] diff --git a/tests/utilities/test_logging.py b/tests/utilities/test_logging.py index a8a7305c4..d9f0eb22f 100644 --- a/tests/utilities/test_logging.py +++ b/tests/utilities/test_logging.py @@ -6,7 +6,7 @@ import os import unittest -from tests._support import load_module, purge_modules +from tests._support import load_module, purge_modules, reimport_once_per_class RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) @@ -34,7 +34,8 @@ class LoggingEnvironmentTests(unittest.TestCase): """The environment-variable flags, which are read at import time.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + self._saved = {key: os.environ.get(key) for key in ('PCAPKIT_DEVMODE', 'PCAPKIT_VERBOSE', 'PCAPKIT_SPHINX')} def tearDown(self) -> None: @@ -88,7 +89,8 @@ class LoggingImportTimeTests(unittest.TestCase): """Importing a library must not configure the application's logging.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + self._saved = os.environ.get('PCAPKIT_DEVMODE') os.environ.pop('PCAPKIT_DEVMODE', None) pristine() @@ -181,7 +183,8 @@ class LoggerHierarchyTests(unittest.TestCase): """Per-module loggers, so a consumer can address one subtree at a time.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + pristine() def tearDown(self) -> None: @@ -237,7 +240,8 @@ class LoggingConfigureTests(unittest.TestCase): """The public configuration API, at runtime rather than at import.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + pristine() def tearDown(self) -> None: @@ -391,7 +395,8 @@ class RegistryLogLevelTests(unittest.TestCase): """Registration bookkeeping is the library's own business, so ``debug``.""" def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + pristine() def tearDown(self) -> None: @@ -452,7 +457,8 @@ class ExtractorLoggingTests(unittest.TestCase): """ def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) + pristine() def tearDown(self) -> None: diff --git a/tests/vendor/test_crawler_reachability_unit.py b/tests/vendor/test_crawler_reachability_unit.py index 5edd40362..b84398ba1 100644 --- a/tests/vendor/test_crawler_reachability_unit.py +++ b/tests/vendor/test_crawler_reachability_unit.py @@ -71,7 +71,7 @@ from collections import Counter from typing import TYPE_CHECKING -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -115,7 +115,7 @@ class CrawlerReachabilityTests(unittest.TestCase): Vendor: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import pcapkit import pcapkit.vendor as vendor diff --git a/tests/vendor/test_ftp_return_code_unit.py b/tests/vendor/test_ftp_return_code_unit.py index 89b9b1ba0..ad827cbc0 100644 --- a/tests/vendor/test_ftp_return_code_unit.py +++ b/tests/vendor/test_ftp_return_code_unit.py @@ -39,7 +39,7 @@ import unittest from typing import TYPE_CHECKING -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -103,7 +103,7 @@ class FTPReturnCodeEmptyRowTests(unittest.TestCase): vendor_module: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import pcapkit.vendor.ftp.return_code as vendor_module diff --git a/tests/vendor/test_ipx_packet_unit.py b/tests/vendor/test_ipx_packet_unit.py index 2db7e7949..49ee5403a 100644 --- a/tests/vendor/test_ipx_packet_unit.py +++ b/tests/vendor/test_ipx_packet_unit.py @@ -35,7 +35,7 @@ import unittest from typing import TYPE_CHECKING -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -115,7 +115,7 @@ class IPXPacketVendorTests(unittest.TestCase): const_module: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import pcapkit.const.ipx.packet as const_module import pcapkit.vendor.ipx.packet as vendor_module diff --git a/tests/vendor/test_ipx_socket_unit.py b/tests/vendor/test_ipx_socket_unit.py index da3449c55..aa62341fe 100644 --- a/tests/vendor/test_ipx_socket_unit.py +++ b/tests/vendor/test_ipx_socket_unit.py @@ -29,7 +29,7 @@ import unittest from typing import TYPE_CHECKING -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -181,7 +181,7 @@ class IPXSocketVendorTests(unittest.TestCase): const_module: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import pcapkit.const.ipx.socket as const_module import pcapkit.vendor.ipx.socket as vendor_module diff --git a/tests/vendor/test_request_prompt_unit.py b/tests/vendor/test_request_prompt_unit.py index ac46713f8..4e9bf1315 100644 --- a/tests/vendor/test_request_prompt_unit.py +++ b/tests/vendor/test_request_prompt_unit.py @@ -83,7 +83,7 @@ from typing import TYPE_CHECKING from unittest import mock -from tests._support import purge_modules, time_limit +from tests._support import reimport_once_per_class, time_limit if TYPE_CHECKING: from typing import Any, Iterator @@ -224,7 +224,7 @@ class StdinInteractivityTests(unittest.TestCase): default: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import pcapkit.vendor.default as default @@ -278,7 +278,7 @@ class ManualIntervenePromptTests(unittest.TestCase): requests: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import requests diff --git a/tests/vendor/test_user_agent_unit.py b/tests/vendor/test_user_agent_unit.py index 400a2392e..7725a8d14 100644 --- a/tests/vendor/test_user_agent_unit.py +++ b/tests/vendor/test_user_agent_unit.py @@ -45,7 +45,7 @@ from typing import TYPE_CHECKING from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any, Iterator @@ -137,7 +137,7 @@ class VendorUserAgentTests(unittest.TestCase): requests: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import requests diff --git a/tests/vendor/test_vendor_dest_path_unit.py b/tests/vendor/test_vendor_dest_path_unit.py index d8a9bedcd..3d7cd7c5a 100644 --- a/tests/vendor/test_vendor_dest_path_unit.py +++ b/tests/vendor/test_vendor_dest_path_unit.py @@ -118,7 +118,7 @@ from typing import TYPE_CHECKING from unittest import mock -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -190,7 +190,7 @@ class ExistingCrawlerDestPathTests(unittest.TestCase): Vendor: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import pcapkit import pcapkit.vendor as vendor @@ -278,7 +278,7 @@ class NestedCrawlerDestPathTests(unittest.TestCase): Vendor: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import pcapkit import pcapkit.vendor as vendor diff --git a/tests/vendor/test_vendor_ipv6_extension_header_925_unit.py b/tests/vendor/test_vendor_ipv6_extension_header_925_unit.py index 273a27a02..b6ac80a33 100644 --- a/tests/vendor/test_vendor_ipv6_extension_header_925_unit.py +++ b/tests/vendor/test_vendor_ipv6_extension_header_925_unit.py @@ -65,7 +65,7 @@ import unittest from typing import TYPE_CHECKING -from tests._support import purge_modules +from tests._support import reimport_once_per_class if TYPE_CHECKING: from typing import Any @@ -195,7 +195,7 @@ class ExtensionHeaderVendorTests(unittest.TestCase): vendor_module: 'Any' def setUp(self) -> None: - purge_modules(['pcapkit']) + reimport_once_per_class(self) import pcapkit.vendor.ipv6.extension_header as vendor_module