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
6 changes: 6 additions & 0 deletions docs/source/pcapkit/const/mh.rst
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,12 @@ Link-Layer Address (LLA) Option Code
This module contains the constant enumeration for **Link-Layer Address (LLA) Option Code**,
which is automatically generated from :class:`pcapkit.vendor.mh.lla_code.LLACode`.

.. versionchanged:: 1.5.0

Code ``0`` is ``Wildcard`` and code ``2`` is ``MN``, as
:rfc:`5568#section-6.4.3` names them, where they were ``Wilcard`` and
``MH``. Neither old name is kept as an alias (:issue:`1848`).

.. autoclass:: pcapkit.const.mh.lla_code.LLACode
:members:
:undoc-members:
Expand Down
4 changes: 2 additions & 2 deletions pcapkit/const/mh/lla_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
class LLACode(EnumRegistry, IntEnum):
"""[LLACode] Link-Layer Address (LLA) Option Code"""

Wilcard = 0
Wildcard = 0

New_Access_Point = 1

MH = 2
MN = 2

NAR = 3

Expand Down
4 changes: 2 additions & 2 deletions pcapkit/protocols/internet/mh.py
Original file line number Diff line number Diff line change
Expand Up @@ -3258,7 +3258,7 @@ def _read_opt_lla(self, schema: 'Schema_LinkLayerAddressOption', *,
Constructed option data.

"""
if schema.code != Enum_LLACode.MH:
if schema.code != Enum_LLACode.MN:
raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format')

data = Data_LinkLayerAddressOption(
Expand Down Expand Up @@ -8106,7 +8106,7 @@ def _make_opt_lla(self, type: 'Enum_Option', option: 'Optional[Data_LinkLayerAdd
return Schema_LinkLayerAddressOption(
type=type,
length=len(address) + 1,
code=Enum_LLACode.MH, # type: ignore[arg-type]
code=Enum_LLACode.MN, # type: ignore[arg-type]
lla=address,
)

Expand Down
4 changes: 2 additions & 2 deletions pcapkit/vendor/mh/lla_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
#: LLA option codes.
DATA = {
#: Wildcard requesting resolution for all nearby access points.
0: 'Wilcard',
0: 'Wildcard',
#: Link-Layer Address of the New Access Point.
1: 'New Access Point',
#: Link-Layer Address of the MN.
2: 'MH',
2: 'MN',
#: Link-Layer Address of the NAR (i.e., Proxied Originator).
3: 'NAR',
#: Link-Layer Address of the source of RtSolPr or PrRtAdv.
Expand Down
49 changes: 49 additions & 0 deletions tests/const/test_const_mh_lla_code_1848_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
"""``LLACode`` names codes ``0`` and ``2`` as :rfc:`5568#section-6.4.3` does.

GitHub issue #1848. Code ``0`` was misspelt ``Wilcard``, and code ``2``, the
*Link-Layer Address of the MN*, was named ``MH``. Both are renamed, to
``Wildcard`` and ``MN``, and neither old name survives as an alias, as the
renames of #1090 and #1715 left none.

"""
import unittest


class LLACodeRenameTests(unittest.TestCase):
"""The RFC's names resolve both ways, and the shipped names are gone."""

def test_code_0_is_wildcard(self) -> 'None':
from pcapkit.const.mh.lla_code import LLACode

self.assertEqual(LLACode.Wildcard, 0)
self.assertEqual(LLACode(0).name, 'Wildcard')
self.assertIs(LLACode['Wildcard'], LLACode(0))

def test_code_2_is_mn(self) -> 'None':
from pcapkit.const.mh.lla_code import LLACode

self.assertEqual(LLACode.MN, 2)
self.assertEqual(LLACode(2).name, 'MN')
self.assertIs(LLACode['MN'], LLACode(2))

def test_the_shipped_names_are_not_aliases(self) -> 'None':
from pcapkit.const.mh.lla_code import LLACode

for name in ('Wilcard', 'MH'):
with self.subTest(name=name):
self.assertFalse(hasattr(LLACode, name))
self.assertNotIn(name, LLACode.__members__)

def test_the_class_docstring_carries_no_version_directive(self) -> 'None':
# The module is generated, so the rename is noted on the docs page.
from pcapkit.const.mh.lla_code import LLACode

doc = LLACode.__doc__ or ''
for directive in ('.. versionadded::', '.. versionchanged::', '.. versionremoved::'):
with self.subTest(directive=directive):
self.assertNotIn(directive, doc)


if __name__ == '__main__':
unittest.main()
6 changes: 3 additions & 3 deletions tests/protocols/internet/test_mh_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ def test_mh_option_readers_cover_known_options_and_guards(self) -> None:
).prefix), '2001:db8::/64')
self.assertEqual(proto._read_opt_lla(
schema.LinkLayerAddressOption(type=Option.Mobility_Header_Link_Layer_Address_option,
length=7, code=LLACode.MH, lla=b'abcdef'),
length=7, code=LLACode.MN, lla=b'abcdef'),
options=options,
).lla, b'abcdef')
self.assertEqual(proto._read_opt_mn_id(
Expand Down Expand Up @@ -798,7 +798,7 @@ def make_extension(code: CGAExtension, extension: data.UnknownExtension | None =
self.assertEqual(proto._make_opt_lla(
Option.Mobility_Header_Link_Layer_Address_option,
data.LinkLayerAddressOption(type=Option.Mobility_Header_Link_Layer_Address_option,
length=7, code=LLACode.MH, lla=b'abcdef'),
length=7, code=LLACode.MN, lla=b'abcdef'),
).lla, b'abcdef')
self.assertEqual(proto._make_opt_mn_id(
Option.MN_ID_OPTION_TYPE,
Expand Down Expand Up @@ -1298,7 +1298,7 @@ def parse(hexstr: str) -> object:
)
self.assertEqual(fna.type, Packet.Fast_Neighbor_Advertisement)
self.assertEqual(fna.length, 24)
self.assertEqual(fna.options[mhlla_type].code, LLACode.MH)
self.assertEqual(fna.options[mhlla_type].code, LLACode.MN)
self.assertEqual(fna.options[mhlla_type].lla, b'\x00\x11\x22\x33\x44\x55')
self.assertEqual(fna.options[Option.PadN].length, 7)

Expand Down
118 changes: 118 additions & 0 deletions tests/vendor/test_vendor_mh_lla_code_1848_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
"""The ``LLACode`` crawler renders the committed const module, renames included.

GitHub issue #1848. :mod:`pcapkit.vendor.mh.lla_code` hand-writes its table, so
its generator runs offline: :meth:`~pcapkit.vendor.mh.lla_code.LLACode.request`
returns :data:`~pcapkit.vendor.mh.lla_code.DATA`. This suite drives the
crawler's :meth:`count`, :meth:`process` and :meth:`context` on that table,
without :meth:`~pcapkit.vendor.default.Vendor.__init__`'s file write, and
checks that the result is the committed :mod:`pcapkit.const.mh.lla_code`
byte for byte, so the template and the module cannot drift apart.

"""
from __future__ import annotations

import importlib.util
import pathlib
import unittest
from typing import TYPE_CHECKING

from tests._support import reimport_once_per_class

if TYPE_CHECKING:
from typing import Any

#: Repository root, i.e. the grandparent of the directory holding this file.
ROOT = pathlib.Path(__file__).resolve().parents[2]

#: The committed module the crawler generates.
CONST = ROOT / 'pcapkit' / 'const' / 'mh' / 'lla_code.py'

#: Every distribution importing :mod:`pcapkit.vendor` needs, as
#: :file:`tests/vendor/test_user_agent_unit.py` explains.
VENDOR_DEPS = ('requests', 'bs4', 'html5lib')

#: Whether the crawlers are importable at all.
HAS_VENDOR_DEPS = all(importlib.util.find_spec(name) is not None for name in VENDOR_DEPS)


def _normalize(context: 'str') -> 'str':
"""Apply the whitespace normalisation :meth:`Vendor.__init__` writes with.

Args:
context: Return value of :meth:`~pcapkit.vendor.default.Vendor.context`.

Returns:
The text as the generator would have written it to disk.

"""
lines = [] # type: list[str]
for line in context.splitlines():
if line:
if line.strip():
lines.append(line.rstrip())
else:
lines.append(line)
return '\n'.join(lines) + '\n'


@unittest.skipUnless(HAS_VENDOR_DEPS, f'vendor extra not installed ({", ".join(VENDOR_DEPS)})')
class LLACodeVendorTests(unittest.TestCase):
"""The hand-written table and its template, rendered without a write."""

if TYPE_CHECKING:
vendor_module: 'Any'

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

import pcapkit.vendor.mh.lla_code as vendor_module

resolved = pathlib.Path(vendor_module.__file__).resolve()
if ROOT not in resolved.parents:
self.skipTest(f'{vendor_module.__name__} was imported from {resolved}, which is '
f'outside {ROOT}; install this checkout with `pip install -e .` to '
f'run this suite against it')

self.vendor_module = vendor_module

def _vendor(self) -> 'tuple[Any, dict[int, str]]':
"""A crawler with the attributes ``__init__`` sets, but none of its I/O."""
cls = self.vendor_module.LLACode
vendor = cls.__new__(cls)
vendor.NAME = cls.__name__
vendor.DOCS = cls.__doc__
data = vendor.request()
vendor.record = vendor.count(data)
return vendor, data

def test_the_table_carries_the_rfc_names(self) -> 'None':
vendor, data = self._vendor()
self.assertEqual(data[0], 'Wildcard')
self.assertEqual(data[2], 'MN')

enum, _ = vendor.process(data)
members = [entry.strip() for entry in enum]
self.assertIn('Wildcard = 0', members)
self.assertIn('MN = 2', members)
self.assertNotIn('Wilcard = 0', members)
self.assertNotIn('MH = 2', members)

def test_context_renders_no_version_directive(self) -> 'None':
# Generated modules carry no version directives: the scripts
# regenerate them, so the rename is noted on the docs page instead.
vendor, data = self._vendor()
context = vendor.context(data)
self.assertIn(' """[LLACode] Link-Layer Address (LLA) Option Code"""\n', context)
for directive in ('.. versionadded::', '.. versionchanged::', '.. versionremoved::'):
with self.subTest(directive=directive):
self.assertNotIn(directive, context)

def test_context_matches_the_committed_const_file_byte_for_byte(self) -> 'None':
vendor, data = self._vendor()
rendered = _normalize(vendor.context(data))
self.assertEqual(rendered, CONST.read_text(encoding='utf-8'))


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