Skip to content

tests: an unrestored setUpClass module purge leaks sys.modules state across files, blocking --dist loadfile #720

Description

@JarryShaw

pytest -n auto is safe with the default --dist load, but --dist loadfile and --dist loadscope surface a real order-dependent failure. The cause is a setUpClass module purge that nothing restores, and it is a latent defect under serial pytest too — parallelism only makes it likely.

The failure

tests/protocols/transport/test_tcp_sack_length_unit.py:212
AssertionError: 'pcapkit.corekit.fields.misc' not found in sys.modules

Reproduced with no xdist at all, two files, serial, in this order:

Order Result
test_dispatch_registry_unit.py → test_tcp_sack_length_unit.py 1 failed, 10 passed
test_enum_schema_registry_unit.py → test_tcp_sack_length_unit.py 8 passed

Origin

tests/protocols/test_dispatch_registry_unit.py:102-104:

@classmethod
def setUpClass(cls) -> None:
    purge_modules(['pcapkit'])
    cls.dispatch = _load_generator()

No tearDownClass or addClassCleanup in the file.

tests/conftest.py:95-153's autouse restore_module_table is function-scoped, and its own docstring states the ordering: setUpClass runs inside the snapshot, because pytest instantiates class-scoped fixtures before function-scoped ones. So the purge is captured into the snapshot and restored to rather than undone. The fixture preserves whatever state it finds on entry; it does not heal a bad one. With no tearDownClass, the class exits leaving pcapkit.* absent, and every later test's snapshot faithfully preserves that absence.

_load_generator() loads examples/generators/dispatch.py, whose module-level imports are __future__, os, struct, tempfile, typing — no pcapkit. So nothing re-imports the package and nothing heals the gap.

The discriminator is the loader, not the teardown

purge_modules appears at 159 sites across 104 files — 149 in setUp, 10 in setUpClass — and 75 of those files have no teardown of any kind. That is the norm, and it is safe: a setUp purge runs after the snapshot and is undone.

Only the 10 setUpClass sites can leak, and whether they do depends on whether their loader re-imports pcapkit at module level:

  • tests/protocols/test_dispatch_registry_unit.py:103 — loader does not import it. Leaks.
  • tests/protocols/test_option_roundtrip_unit.py:588 — examples/generators/options.py imports pcapkit.protocols.protocol at module level, which transitively reimports pcapkit.corekit.fields.misc before the first snapshot. Self-heals, does not reproduce (10 passed, 370 subtests).

The remaining 7 setUpClass sites were not tested against this failure, so the blast radius is unmeasured.

A second, independent defect

tests/const/test_const_enum_get.py:154 and tests/const/test_const_enum_builtin_parity.py:199 both call:

cls.addClassCleanup(purge_modules, ['pcapkit'])

That purges again on the way out; it does not restore. Same leak shape, reached deliberately rather than by omission. The comment above :199 says the intent is "so that pollution cannot reach another module", which is the opposite of what a second purge achieves.

Suggested fix

Prefer the architectural one — the per-file fixes have already proven insufficient once, in #660:

  1. Make restore_module_table restore to a pinned known-good snapshot rather than to whatever state it found on entry. That heals a bad state instead of preserving it, and covers every current and future site.
  2. Or make the test_tcp_sack_length_unit.py:212 precondition self-healing — re-import if absent — instead of a hard assertIn. Narrower, and it leaves the underlying leak in place for the next victim.

Either way, the two addClassCleanup sites want changing to restore rather than purge, and the 7 untested setUpClass sites want auditing against the loader criterion above.

Why it matters beyond xdist

-n auto --dist load measured ~3.6–3.9× on tests/corekit, tests/protocols and tests/foundation with counts byte-identical to serial. loadfile/loadscope cannot be adopted until this is fixed. See #715 for the wider CI work.

Activity

  1. added
    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)
    testPull requests that add or correct tests (test: subject prefix)
    on Sep 23, 2026
  2. JarryShaw commented on Sep 23, 2026

    @JarryShaw
    OwnerAuthor

    Fixed in 4391dc7.

    Route chosen: architectural (option 1) — restore_module_table now restores
    to a snapshot pinned once at session start, both before and after every test,
    instead of one taken at each test's own entry. Closes the leak from either
    side of a class (setUpClass purge with no teardown, or a class-cleanup that
    purges again) regardless of whether the class's loader re-imports pcapkit.

    Also fixed: the two named addClassCleanup(purge_modules, ...) sites, plus a
    third, unnamed one in test_const_enum_lookup.py with the same defect — all
    now restore instead of purging again.

    Before: test_dispatch_registry_unit.py → test_tcp_sack_length_unit.py =
    1 failed, 10 passed. After: 11 passed.

    Verified: tests/const (40), tests/protocols (771), tests/project (126) all
    pass — 937 passed, 0 failures — including the #660 regression guard.

  3. added this to the 1.5 milestone on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)testPull requests that add or correct tests (test: subject prefix)

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions