diff --git a/docs/source/pcapkit/const/mh.rst b/docs/source/pcapkit/const/mh.rst index aaeb0c788..3a75b1271 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 8006842ea..24165ac60 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 96a7e7e05..954733238 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 8d9d61321..9f93cf57c 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 000000000..913eb09c1 --- /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 238cfe8d9..68ab9b733 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 000000000..98419ed3e --- /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()