Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions tests/_dependency_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@
# :file:`pyproject.toml` extra declares it at all -- so
# :func:`extras_providing` resolves it to the empty set and every pytest
# job reaching a ``HAS_MYPY`` gate is reported as a :class:`Gap`. That is
# the honest answer rather than the :exc:`KeyError` a missing entry used to
# raise: the gate really is dark in CI, and
# the honest answer rather than a :exc:`KeyError` for a missing entry: the
# gate really is dark in CI, and
# :data:`DEPENDENCY_GATE_EXCLUSIONS`'s ``HAS_MYPY`` entry is where the reason
# for declining to close it lives (#779). Spelled as a requirement string
# anyway, so the day some extra does carry mypy the gap closes itself
Expand All @@ -190,8 +190,8 @@
# (#766), so :func:`extras_providing` resolves it to ``{'test'}``, every
# pytest job installs it, and ``HAS_ISORT`` is a satisfied gate with no
# :data:`DEPENDENCY_GATE_EXCLUSIONS` entry at all -- which is the whole point
# of listing it here: the guard now *counts* a gate it could not see before,
# and finds nothing to report.
# of listing it here: the guard *counts* a gate it could not see without
# the entry, and finds nothing to report.
#
# Why the opposite ruling from ``mypy``, whose gap is declined above: that
# entry's reason turns on lint.yml already running mypy over the whole
Expand Down Expand Up @@ -539,7 +539,7 @@ class Job(NamedTuple):
That is deliberate rather than an oddity to special-case away:
:func:`dependency_gate_gaps` aggregates by ``(flag, job.name)``, and a gate
reached by *any* of a job's per-engine variants is exactly what "this job
reaches this gate" has to mean once one job's cells no longer all install
reaches this gate" has to mean when one job's cells do not all install
and select the same thing.

"""
Expand Down
39 changes: 18 additions & 21 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,22 @@ def pytest_sessionstart(session: 'pytest.Session') -> 'None':
happens here decides what every test is restored *to*. Left cold, the
region is empty at the pin and restored to empty after every test, which
makes each of the tests that does not purge for itself -- :mod:`tests.project`
is the bulk of them -- pay a fresh ``import pcapkit`` it used to get from the
warm table. Measured over ``tests/project/``: 1.59s with no
is the bulk of them -- pay a fresh ``import pcapkit`` it would otherwise get
from the warm table. Measured over ``tests/project/``: 1.59s with no
guard at all, **9.34s** guarded from a cold table, **0.35s** guarded from a
warm one. The last is the fastest of the three because the import it would
otherwise do inside the first test has moved here instead.

This used **not** to help a class that purges in
:meth:`~unittest.TestCase.setUpClass`, for the reason
:func:`_pin_module_snapshot` explains: such a class purges *after* this hook
and *before* the first snapshot the old, entry-scoped fixture took, so that
snapshot was whatever the class left -- warming the table earlier changed
nothing about it. Restoring to a *pinned* snapshot before each test as well
as after, rather than to one taken at that test's own entry, is what makes
the warm-up matter to a purging class too now: the pin below still captures
this import, and every later test -- including a purging class's own first
one -- is restored to it going in as well as coming out, regardless of
whether that class reloads anything for itself.
It helps a class that purges in :meth:`~unittest.TestCase.setUpClass` too.
Such a class purges *after* this hook and *before* its first test, so a
snapshot taken at that test's own entry would be whatever the class left --
warming the table earlier would change nothing about it, as
:func:`_pin_module_snapshot` explains. Restoring to a *pinned* snapshot
before each test as well as after is what makes the warm-up matter to a
purging class: the pin below captures this import, and every later test --
including a purging class's own first one -- is restored to it going in as
well as coming out, regardless of whether that class reloads anything for
itself.
So a class that purges and reimports for itself, as
:class:`tests.integration._helpers.EndToEndTestCase` does, still runs its
tests on the pin. Only a module object a class attribute holds a direct
Expand Down Expand Up @@ -269,13 +268,11 @@ def restore_module_table() -> 'Iterator[None]':
that will be written, which is the difference between the failure being
unlikely and being impossible.

The others were fixed at their call sites when this guard landed, but
:mod:`tests.cli.test_main` was not: it kept its own purge loop, which
purges and restores nothing, and this fixture went on quietly healing it for
every test. That is issue #688 -- it took an audit rather than a failing run
to find, because a guard that repairs a leak also hides it. It is now fixed
at its call site too, which is what makes the paragraph above true of all
three rather than of two.
A guard that repairs a leak also hides it. Issue #688 -- a purge loop in
:mod:`tests.cli.test_main` that purged and restored nothing, which this
fixture quietly healed for every test -- took an audit rather than a
failing run to find. That is why every leak is fixed at its call site as
well.

Deliberately *not* fixed by moving ``test_protochain.py`` so it sorts
elsewhere, or by leaning on a neighbouring test's purge to heal the state.
Expand Down Expand Up @@ -603,7 +600,7 @@ def _tier_guard_lock(exclusive: 'bool') -> 'Iterator[None]':
writes a real violation to disk, under :data:`~tests._tiers.PROBE_MODULE_NAME`, to
reproduce the tier guard's own collection-time diagnostic end to end.
Serially the two never overlap; under the ``-n auto --dist load`` this
suite's CI jobs now use, pytest-xdist may hand the two tests to different
suite's CI jobs use, pytest-xdist may hand the two tests to different
worker processes at the same moment, and the scan can then observe the
probe mid-life and report a violation nobody committed -- a real defect in
the test suite's own concurrency, not a false positive, since the guard is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
GitHub issues #1502 and #1574. :meth:`DPKT._get_protocol
<pcapkit.foundation.engines.dpkt.DPKT._get_protocol>` knew Ethernet, IPv4 and
IPv6 only, so a ``LINKTYPE_RAW`` (101), ``LINKTYPE_NULL`` (0) or
``LINKTYPE_LOOP`` (108) frame was kept as raw data with a warning. Raw IP is now
``LINKTYPE_LOOP`` (108) frame was kept as raw data with a warning. Raw IP is
read as IPv4 or IPv6 by its version nibble, and both loopback link types by the
address family, as :class:`~pcapkit.protocols.link.loopback.Loopback` reads
them, with the network layer where :mod:`pcapkit.toolkit.dpkt` looks for it.
Expand Down
45 changes: 21 additions & 24 deletions tests/foundation/engines/test_engine_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,22 @@ def read_frame(self) -> str:
def test_engine_subclass_registration_is_opt_in(self) -> None:
"""Registration happens if and only if ``engine`` is given.

This is the #514 opt-in contract. Before it, ``__init_subclass__`` fell
back to ``cls.name`` when the keyword was absent, so *every* subclass of
the public class was registered -- which is why no built-in could
subclass it and every one of them inherited :class:`EngineBase` under an
alias instead.
This is the #514 opt-in contract. It guards against ``__init_subclass__``
falling back to ``cls.name`` when the keyword is absent, which registers
*every* subclass of the public class.

The ``Default`` case is the load-bearing half: it sets
``__engine_name__``, so under the old fallback it registered under
``'defaultengine'``. A class attribute is not a registry key, and it
``__engine_name__``, so a fallback to ``cls.name`` would register it under
``'defaultengine'``. A class attribute is not a registry key, so it
registers nothing.

No version guard, and that is the point of the rename. The keyword was
``name`` when opt-in registration landed, which made this half of the test
unrunnable below Python 3.11 -- ``mcls``/``name``/``bases``/``namespace``
collide with :meth:`abc.ABCMeta.__new__`'s own parameters, which are
positional-or-keyword before 3.11 and positional-only from 3.11, so the
class statement failed in the metaclass before ``__init_subclass__`` ran.
``engine`` is outside that set, so this runs everywhere. Verified on
3.10.21 and 3.14.7.
No version guard, and that is why the keyword is ``engine`` rather than
``name``: ``mcls``/``name``/``bases``/``namespace`` collide with
:meth:`abc.ABCMeta.__new__`'s own parameters, which are
positional-or-keyword before 3.11 and positional-only from 3.11, so below
Python 3.11 such a class statement fails in the metaclass before
``__init_subclass__`` runs. ``engine`` is outside that set, so this runs
everywhere. Verified on 3.10.21 and 3.14.7.

"""
from pcapkit.foundation.engines.engine import Engine
Expand Down Expand Up @@ -123,10 +120,9 @@ def test_engine_subclass_rejects_unrecognised_keyword(self) -> None:

``Engine`` spells the registry key ``engine`` while ``Reassembly`` and
``TraceFlow`` spell the same idea ``protocol``, so guessing the wrong one
is the expected mistake. It used to land in ``**kwargs``, get dropped by
the bare ``super().__init_subclass__()``, and leave the class registered
under its own class name. With registration now opt-in the same typo
would instead skip registration silently, which is quieter still.
is the expected mistake. Without the guard it lands in ``**kwargs``, is
dropped by a bare ``super().__init_subclass__()``, and, registration being
opt-in, the class silently goes unregistered.

Unguarded by version on purpose: ``protocol`` is not one of the four
names that collide with :meth:`abc.ABCMeta.__new__` below Python 3.11, so
Expand All @@ -138,7 +134,7 @@ def test_engine_subclass_rejects_unrecognised_keyword(self) -> None:
version-dependent: below 3.11 the metaclass collision fires before the
guard, so it is a :exc:`TypeError` rather than an
:exc:`~pcapkit.utilities.exceptions.UnsupportedCall`. Loud either way,
which is why the rename did not need to chase it.
which is why the guard needs no special case for it.

"""
from pcapkit.foundation.engines.engine import Engine
Expand Down Expand Up @@ -179,17 +175,18 @@ def test_registration_is_not_inherited_by_a_subclass(self) -> None:
``final`` marker -- so that subclassing them again cannot register the
subclass under a name nobody chose. That worked, but it is a property of
*which base you inherited*, so it cannot be varied per subclass, and
under the old fallback a subclass of a registered class re-registered
itself under its own class name.
under a ``cls.name`` fallback a subclass of a registered class
re-registers itself under its own class name.

Keying registration on the keyword instead gives both properties at once:
the parent stays registered, the subclass does not re-register, and a
subclass that *wants* registering can still ask for it. Which is why
``HTTP``, ``L2TP`` and ``IP`` can be open to inheritance without the
anti-re-registration guarantee being weakened.

Runs on every supported version since the keyword became ``engine``;
it was skipped below 3.11 while the keyword was ``name``.
Runs on every supported version: ``engine`` does not collide with
:meth:`abc.ABCMeta.__new__`'s own parameters below 3.11, as ``name``
would.

"""
from pcapkit.foundation.engines.engine import Engine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
:class:`~pcapkit.foundation.extraction.Extractor` keeps a callable ``verbose``
as its ``_vfunc``, but each third-party engine installed its own printer
whenever verbose output was on, so the caller's callback was replaced and never
called. An engine now installs its printer only for ``verbose=True``.
called. An engine installs its printer only for ``verbose=True``.

Each case runs a real extraction of :file:`in.pcap`, which git tracks, so the
module is unit tier. It proves first that the requested engine ran: the
Expand Down
8 changes: 4 additions & 4 deletions tests/foundation/engines/test_pcapng_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,10 +414,10 @@ def test_packet_blocks_without_an_interface_description_are_format_errors(self)
self.assertIn('interface description block', message)

def test_out_of_range_interface_id_is_a_format_error(self) -> None:
# Regression for #367: a section with at least one IDB and a block naming
# an interface it does not describe used to escape as a bare
# ``IndexError`` out of ``PCAPNG._get_timezone``, because the engine's
# bounds guards ran only after the block had already been parsed.
# Regression for #367: guards against a section with at least one IDB and
# a block naming an interface it does not describe escaping as a bare
# ``IndexError`` out of ``PCAPNG._get_timezone``, which happens when the
# engine's bounds guards run only after the block has been parsed.
from pcapkit.utilities.exceptions import FormatError

blocks = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
0's snaplen, and a snaplen of 0 means no limit. The default engine handed
interface 0's snaplen to the parser as ``__packet__['snaplen']`` as it stood, so
a 0 read as a limit of no octets, and the block's padding was read as packet
data: 57 data octets came out as 60. The engine now hands over no limit for a
data: 57 data octets came out as 60. The engine hands over no limit for a
zero snaplen, and :class:`~pcapkit.protocols.misc.pcapng.PCAPNG` reads a zero
snaplen handed in as no limit too.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

#: What the fake ``pcap`` package raises on import: the libpcap loader's message.
LOADER_ERROR = 'Cannot find libpcap.so library'
#: The verdict ``PCAP_CT`` already gave on that failure, which ``PyPCAP`` shares.
#: The verdict ``PCAP_CT`` and ``PyPCAP`` both give on that failure.
REASON = f'the `pcap` module is installed but unusable -- OSError: {LOADER_ERROR}'
#: Hang guard for the child interpreter, in seconds.
TIMEOUT = 120
Expand Down
2 changes: 1 addition & 1 deletion tests/foundation/engines/test_pyshark_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* **the** :program:`tshark` **binary** -- ``pyshark`` shells out to it and parses
nothing itself.

What the running host provides is no longer assumed either way, and that is worth
What the running host provides is not assumed either way, and that is worth
stating plainly rather than leaving to a silent skip: :program:`tshark` is absent
on most of this file's legs, but #751's ``engine-tests`` job -- which collects this
same module -- installs it via ``apt-get``, so neither "present" nor "absent" can
Expand Down
2 changes: 1 addition & 1 deletion tests/foundation/engines/test_scapy_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
public API resets them, and ``sys.modules`` surgery does not un-run a
``bind_layers`` call. So an in-process assertion here would pass whenever
something earlier in the run had imported a `Scapy`_ layer -- which is exactly
what happened before #406 was found. :mod:`tests.toolkit.test_scapy_unit` imports
how #406 went unnoticed. :mod:`tests.toolkit.test_scapy_unit` imports
:mod:`scapy.layers.l2`, :mod:`~scapy.layers.inet` and :mod:`~scapy.layers.inet6`
to build its fixtures, so ``pytest tests/toolkit tests/interface/test_misc.py``
saw correct dissection while ``pytest tests/interface/test_misc.py`` alone did
Expand Down
4 changes: 2 additions & 2 deletions tests/foundation/reassembly/data/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ def test_ip_data_models_and_package_aliases(self) -> None:
self.assertEqual(storage.tcp, ())

def test_completion_is_a_string_that_still_reads_as_the_old_bool(self) -> None:
"""The contract ``Datagram.completed`` has to keep, now it is a StrEnum.
"""The contract ``Datagram.completed`` has to keep as a StrEnum.

Deriving from :class:`~pcapkit.utilities.compat.StrEnum` -- as
:class:`~pcapkit.protocols.application.httpv1.Type` does -- buys
serialisability and comparison against a plain string. What it must not
cost is the truthiness callers of the former :obj:`bool` field rely on,
cost is the truthiness callers rely on, as for a :obj:`bool` field,
which needs ``__bool__`` overridden because every non-empty string is
otherwise truthy.

Expand Down
6 changes: 3 additions & 3 deletions tests/foundation/reassembly/test_ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ class IPOverlapConflictTests(unittest.TestCase):
:rfc:`791` resolves *which* bytes win itself: "this procedure will use the
more recently arrived copy in the data buffer" -- the opposite of TCP's
first-write-wins (:rfc:`9293#section-3.10`, fixed for TCP by #443/#478).
So every case below still expects the *arriving* fragment's bytes in the
reassembled payload; what changes is that ``datagram.conflict`` now
records where that overwrite disagreed with what was already there.
So every case below expects the *arriving* fragment's bytes in the
reassembled payload, and ``datagram.conflict`` records where that
overwrite disagreed with what was already there.

"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

Every fragment but the last carries a multiple of 8 octets
(:rfc:`8200#section-4.5`, :rfc:`791#section-3.2`), so only the final fragment's
partial last block is now counted received. That covers an IPv4 fragment whose
partial last block is counted received. That covers an IPv4 fragment whose
Total Length declares a non-multiple of 8 on a non-final fragment as well.

Every case builds its fragments in memory and reads no capture. Classes are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
fragment at the largest Fragment Offset, 8191, among them -- made
:meth:`IP._detect_conflicts <pcapkit.foundation.reassembly.ip.IP._detect_conflicts>`
index past the table, and the whole extraction raised :exc:`IndexError`. The
table now has 8,192 entries, the partial last block included.
table has 8,192 entries, the partial last block included.

Every case builds its fragments in memory and reads no capture. Classes are
imported inside each test, after :func:`~tests._support.reimport_once_per_class`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
GitHub issue #1585. The data buffer holds 65,535 octets of data, but IPv4's
Total Length counts the header as well, so fragments whose data covered octets
0-65,522 behind a 20-octet header made a 65,543-octet datagram, and it read
``COMPLETE``. Linux's ``ip_frag_reasm`` rejects it. The bound now counts the
``COMPLETE``. Linux's ``ip_frag_reasm`` rejects it. The bound counts the
header as the family's length field does: IPv4's Total Length counts all of it,
and IPv6's Payload Length counts the extension headers but not the fixed
40-octet header. A Total Length of 0 declares no length (#1555), so a datagram
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
GitHub issue #1566. The data buffer holds 65,535 octets and ``RCVBT`` 8,192
blocks. Both completion checks tested ``all(RCVBT[0:(TDL + 7) // 8])``, and the
slice stops silently at the table's end, so a final fragment declaring more data
than fits completed its datagram short. A datagram now completes only if its
than fits completed its datagram short. A datagram completes only if its
total data length fits the buffer.

A fragment the buffer's end clips counts as cut there: its partial last block is
a hole. An 8-octet non-final fragment at the largest offset therefore received
no block, and strict mode then dropped its datagram, while a 7-octet one was
reported. #1567 leaves the 7-octet one's block clear too, and strict mode now
reports a datagram with no run received, as lax mode does.
a hole. An 8-octet non-final fragment at the largest offset therefore receives
no block, and so does a 7-octet one (#1567). Strict mode reports a datagram with
no run received, as lax mode does.

Every case builds its fragments in memory and reads no capture. Classes are
imported inside each test, after :func:`~tests._support.reimport_once_per_class`.
Expand Down
Loading
Loading