From 5003607bcd0e0e929f9b661370d0d3df35f38d2b Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 16 Oct 2026 19:49:03 -0400 Subject: [PATCH] fix(const)!: name LLACode codes 0 and 2 as RFC 5568 does (#1848) - Rename LLACode.Wilcard to Wildcard and LLACode.MH to MN, in the hand-written vendor table and the generated module alike. No alias keeps either old name: this is a breaking rename. - Note both renames with versionchanged:: 1.5.0 on the const docs page. The generated module carries no version directive, as the crawler regenerates it. - Point the MH-LLA option reader and builder, and their unit tests, at LLACode.MN. - Pin the new names, the absence of the old ones and of any version directive in the generated docstring, and that the crawler renders the committed module byte for byte. Closes #1848 --- docs/source/pcapkit/const/mh.rst | 6 + pcapkit/const/mh/lla_code.py | 4 +- pcapkit/protocols/internet/mh.py | 4 +- pcapkit/vendor/mh/lla_code.py | 4 +- .../const/test_const_mh_lla_code_1848_unit.py | 49 ++++++++ tests/protocols/internet/test_mh_unit.py | 6 +- .../test_vendor_mh_lla_code_1848_unit.py | 118 ++++++++++++++++++ 7 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 tests/const/test_const_mh_lla_code_1848_unit.py create mode 100644 tests/vendor/test_vendor_mh_lla_code_1848_unit.py diff --git a/docs/source/pcapkit/const/mh.rst b/docs/source/pcapkit/const/mh.rst index aaeb0c7881..3a75b12717 100644 --- a/docs/source/pcapkit/const/mh.rst +++ b/docs/source/pcapkit/const/mh.rst @@ -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: diff --git a/pcapkit/const/mh/lla_code.py b/pcapkit/const/mh/lla_code.py index 8006842ea1..24165ac60f 100644 --- a/pcapkit/const/mh/lla_code.py +++ b/pcapkit/const/mh/lla_code.py @@ -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 diff --git a/pcapkit/protocols/internet/mh.py b/pcapkit/protocols/internet/mh.py index 96a7e7e05e..954733238b 100644 --- a/pcapkit/protocols/internet/mh.py +++ b/pcapkit/protocols/internet/mh.py @@ -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( @@ -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, ) diff --git a/pcapkit/vendor/mh/lla_code.py b/pcapkit/vendor/mh/lla_code.py index 8d9d613216..9f93cf57c2 100644 --- a/pcapkit/vendor/mh/lla_code.py +++ b/pcapkit/vendor/mh/lla_code.py @@ -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. diff --git a/tests/const/test_const_mh_lla_code_1848_unit.py b/tests/const/test_const_mh_lla_code_1848_unit.py new file mode 100644 index 0000000000..913eb09c1d --- /dev/null +++ b/tests/const/test_const_mh_lla_code_1848_unit.py @@ -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() diff --git a/tests/protocols/internet/test_mh_unit.py b/tests/protocols/internet/test_mh_unit.py index 238cfe8d91..68ab9b733a 100644 --- a/tests/protocols/internet/test_mh_unit.py +++ b/tests/protocols/internet/test_mh_unit.py @@ -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( @@ -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, @@ -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) diff --git a/tests/vendor/test_vendor_mh_lla_code_1848_unit.py b/tests/vendor/test_vendor_mh_lla_code_1848_unit.py new file mode 100644 index 0000000000..98419ed3e4 --- /dev/null +++ b/tests/vendor/test_vendor_mh_lla_code_1848_unit.py @@ -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()