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
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ Member Shapes
- the member's own name only
- :class:`~pcapkit.protocols.internet.ah.AH` ``('AH',)``,
:class:`~pcapkit.protocols.internet.esp.ESP` ``('ESP',)``;
:class:`IGMPv2 <pcapkit.protocols.internet.igmpv2.IGMP>` ``('IGMPv2',)`` --
:class:`IGMPv2 <pcapkit.protocols.internet.igmpv2.IGMP>` ``('IGMPv2',)`` and
:class:`IGMPv3 <pcapkit.protocols.internet.igmpv3.IGMP>` ``('IGMPv3',)`` --
:class:`IGMPv1 <pcapkit.protocols.internet.igmpv1.IGMP>` narrows the same way,
but to the RFC canonical name rather than a versioned one
(:ref:`protocol-family-canonical-names`)
Expand Down Expand Up @@ -122,11 +123,13 @@ and its ``name`` carries no "version 4". ``ICMPv6`` keeps its own version, since
:rfc:`1112` names IGMPv1's protocol plain ``IGMP``, so
:meth:`IGMPv1.id() <pcapkit.protocols.internet.igmpv1.IGMP.id>` is ``('IGMP',)`` and
its ``alias`` is ``'IGMP'``, while
:meth:`IGMPv2.id() <pcapkit.protocols.internet.igmpv2.IGMP.id>` keeps its own version,
``('IGMPv2',)``, since :rfc:`2236` names it with one. The family base's ``id()``
follows: :meth:`IGMP.id() <pcapkit.protocols.internet.igmp.IGMP.id>` is
``('IGMP', 'IGMPv2')``, the family-inclusive shape, because IGMPv1's canonical name
and the family's own name are the same string -- the same shape
:meth:`IGMPv2.id() <pcapkit.protocols.internet.igmpv2.IGMP.id>` and
:meth:`IGMPv3.id() <pcapkit.protocols.internet.igmpv3.IGMP.id>` keep their own
versions, ``('IGMPv2',)`` and ``('IGMPv3',)``, since :rfc:`2236` and :rfc:`3376` name
them with one. The family base's ``id()`` follows:
:meth:`IGMP.id() <pcapkit.protocols.internet.igmp.IGMP.id>` is
``('IGMP', 'IGMPv2', 'IGMPv3')``, the family-inclusive shape, because IGMPv1's
canonical name and the family's own name are the same string -- the same shape
:meth:`ICMP.id() <pcapkit.protocols.internet.icmp.ICMP.id>` takes, ``('ICMP', 'ICMPv6')``.

.. _protocol-family-selectors:
Expand Down
13 changes: 7 additions & 6 deletions pcapkit/protocols/internet/igmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,22 @@ def length(self) -> 'Literal[8]':
##########################################################################

@classmethod
def id(cls) -> 'tuple[Literal["IGMP"], Literal["IGMPv1"], Literal["IGMPv2"], Literal["IGMPv3"]]':
def id(cls) -> 'tuple[Literal["IGMP"], Literal["IGMPv2"], Literal["IGMPv3"]]':
"""Index ID of the protocol.

:rfc:`1112` names the protocol ``IGMP``, without a version, the same
shape :class:`~pcapkit.protocols.internet.icmp.ICMP` takes for
:rfc:`792` -- so the family base's own name doubles as IGMPv1's
canonical name. :rfc:`2236` names IGMPv2 with its own version.
canonical name. :rfc:`2236` and :rfc:`3376` name IGMPv2 and IGMPv3
with their own versions.

Returns:
Index ID of the protocol, the family name and all three
versions; the concrete IGMPv1, IGMPv2 and IGMPv3 classes narrow
it to their own version.
Index ID of the protocol, the family name and the IGMPv2 and
IGMPv3 names; the concrete IGMPv1, IGMPv2 and IGMPv3 classes
narrow it to their own canonical name.

"""
return ('IGMP', 'IGMPv1', 'IGMPv2', 'IGMPv3')
return ('IGMP', 'IGMPv2', 'IGMPv3')

def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> '_PT':
"""Read Internet Group Management Protocol.
Expand Down
8 changes: 5 additions & 3 deletions pcapkit/protocols/internet/igmpv3.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,16 @@ def length(self) -> 'int':
##########################################################################

@classmethod
def id(cls) -> 'tuple[Literal["IGMP"], Literal["IGMPv3"]]': # type: ignore[override]
def id(cls) -> 'tuple[Literal["IGMPv3"]]': # type: ignore[override]
"""Index ID of the protocol.

:rfc:`3376` names the protocol ``IGMPv3``, carrying its own version.

Returns:
Index ID of the protocol, the family name and the version.
Index ID of the protocol.

"""
return ('IGMP', 'IGMPv3')
return ('IGMPv3',)

def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_IGMP':
"""Read Internet Group Management Protocol version 3.
Expand Down
74 changes: 69 additions & 5 deletions tests/protocols/internet/test_igmp_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,26 +424,33 @@ def test_id_and_version(self) -> None:

self.assertEqual(IGMPv1.id(), ('IGMP',))
self.assertEqual(IGMPv2.id(), ('IGMPv2',))
self.assertEqual(IGMPv3.id(), ('IGMP', 'IGMPv3'))
self.assertEqual(IGMPv3.id(), ('IGMPv3',))
self.assertEqual(object.__new__(IGMPv1).version, 1)
self.assertEqual(object.__new__(IGMPv2).version, 2)
self.assertEqual(object.__new__(IGMPv3).version, 3)

def test_base_id_covers_all_three_versions(self) -> None:
"""The base lists each version's canonical name, and only those: IGMPv1's
is the family name itself, so no ``id()`` carries ``'IGMPv1'``."""
from pcapkit.protocols.internet.igmp import IGMP

self.assertEqual(IGMP.id(), ('IGMP', 'IGMPv1', 'IGMPv2', 'IGMPv3'))
self.assertEqual(IGMP.id(), ('IGMP', 'IGMPv2', 'IGMPv3'))
members = (igmp_class('IGMPv1'), igmp_class('IGMPv2'), igmp_class('IGMPv3'))
self.assertEqual(IGMP.id(), tuple(name for member in members for name in member.id()))
for klass in (IGMP, *members):
with self.subTest(klass=klass.__module__):
self.assertNotIn('IGMPv1', klass.id())

def test_each_version_narrows_the_base_id(self) -> None:
v1, v2, v3 = igmp_class('IGMPv1'), igmp_class('IGMPv2'), igmp_class('IGMPv3')
self.assertEqual(v1.id(), ('IGMP',))
self.assertEqual(v2.id(), ('IGMPv2',))
self.assertEqual(v3.id(), ('IGMP', 'IGMPv3'))
self.assertEqual(v3.id(), ('IGMPv3',))
self.assertNotIn('IGMPv2', v1.id())
self.assertNotIn('IGMPv3', v1.id())
self.assertNotIn('IGMPv1', v2.id())
self.assertNotIn('IGMP', v2.id())
self.assertNotIn('IGMPv3', v2.id())
self.assertNotIn('IGMPv1', v3.id())
self.assertNotIn('IGMP', v3.id())
self.assertNotIn('IGMPv2', v3.id())

def test_igmpv1_parses_its_own_messages_directly(self) -> None:
Expand Down Expand Up @@ -1139,5 +1146,62 @@ def test_ipv4_dispatches_igmp_to_each_version(self) -> None:
self.assertEqual(parsed.payload.alias, expected_alias(version))


class IGMPSelectorTests(unittest.TestCase):
"""Selectors on parsed IGMP layers, per the *Selector Matching* section of
``protocol-family-grouping.rst``: the family name selects every version, and
each version name selects its own (#2512)."""

#: Every IGMP selector: the family name, then each version's registry key.
SELECTORS = ('IGMP', 'IGMPv1', 'IGMPv2', 'IGMPv3')

def setUp(self) -> None:
reimport_once_per_class(self)

@staticmethod
def parse(packet: 'bytes'):
"""Return ``(frame, layer)``: ``packet`` carried by IPv4, and the IGMP
layer IP protocol 2 dispatched it to."""
from pcapkit.protocols.internet.ipv4 import IPv4

outer = IPv4(protocol=2, src='10.0.0.1', dst='224.0.0.1', payload=packet).data
with warnings.catch_warnings():
warnings.simplefilter('ignore')
frame = IPv4(io.BytesIO(outer), len(outer))
return frame, frame.payload

def test_in_and_getitem_select_the_family_and_the_own_version(self) -> None:
"""``in`` and ``[]`` expand the selector, a registry key or its class
alike: ``'IGMP'`` reaches every version, a version key only its own --
``'IGMPv1'`` through its registry key, since no ``id()`` carries it.
The protochain also compares each alias, which here is the same name."""
from pcapkit.protocols import __proto__ as registry
from pcapkit.utilities.exceptions import ProtocolNotFound

for name, (packet, version) in {**IGMP_PACKETS, **IGMPV3_PACKETS}.items():
frame, layer = self.parse(packet)
self.assertIsInstance(layer, igmp_class(version))
for selector in self.SELECTORS:
expected = selector in ('IGMP', version)
for key in (selector, registry[selector.upper()]):
with self.subTest(packet=name, selector=key):
self.assertIs(key in layer, expected)
self.assertIs(key in frame, expected)
self.assertIs(key in frame.protochain, expected)
if expected:
self.assertIs(frame[key], layer)
else:
with self.assertRaises(ProtocolNotFound):
frame[key] # pylint: disable=pointless-statement

def test_string_eq_matches_the_layer_s_own_name_only(self) -> None:
"""String ``==`` is not expanded, so a layer equals its own canonical
name alone: ``'IGMP'`` only on IGMPv1, ``'IGMPv3'`` only on IGMPv3."""
for name, (packet, version) in {**IGMP_PACKETS, **IGMPV3_PACKETS}.items():
_, layer = self.parse(packet)
for selector in self.SELECTORS:
with self.subTest(packet=name, selector=selector):
self.assertIs(layer == selector, selector == expected_alias(version))


if __name__ == '__main__':
unittest.main()
Loading