diff --git a/pcapkit/const/reg/ethertype.py b/pcapkit/const/reg/ethertype.py index 73bca487e8..009bcbc8ac 100644 --- a/pcapkit/const/reg/ethertype.py +++ b/pcapkit/const/reg/ethertype.py @@ -528,13 +528,13 @@ def _missing_(cls, value: 'int') -> 'EtherType': """ if not (isinstance(value, int) and 0x0000 <= value <= 0xFFFF): raise ValueError('%r is not a valid %s' % (value, cls.__name__)) - if 0x0000 <= value <= 0x05DC: - #: IEEE802.3 Length Field [IEEE Std 802.3] - return extend_enum(cls, 'IEEE802_3_Length_Field_0x%s' % hex(value)[2:].upper().zfill(4), value) if 0x0101 <= value <= 0x01FF: #: Old Xerox Experimental values. Invalid as an Ethertype since 1983. [Neil #: Sembower] return cls._unregistered_member(value, 'Old_Xerox_Experimental_values_Invalid_as_an_Ethertype_since_1983') + if 0x0000 <= value <= 0x05DC: + #: IEEE802.3 Length Field [IEEE Std 802.3] + return extend_enum(cls, 'IEEE802_3_Length_Field_0x%s' % hex(value)[2:].upper().zfill(4), value) if 0x0888 <= value <= 0x088A: #: Xyplex [Neil Sembower] return extend_enum(cls, 'Xyplex_0x%s' % hex(value)[2:].upper().zfill(4), value) diff --git a/pcapkit/vendor/reg/ethertype.py b/pcapkit/vendor/reg/ethertype.py index 44a07d1207..111fe50e4f 100644 --- a/pcapkit/vendor/reg/ethertype.py +++ b/pcapkit/vendor/reg/ethertype.py @@ -79,6 +79,34 @@ def rename(self, name: 'str', code: 'str', *, original: 'Optional[str]' = None) name = f'{name}_0x{code}' return self.safe_name(name) + @staticmethod + def _insert_range(ranges: 'list[tuple[int, int, list[str]]]', entry: 'tuple[int, int, list[str]]') -> 'None': + """Insert a range block ahead of the first already-placed range that fully contains it. + + The generated :meth:`~EtherType._missing_` tests these ``if`` blocks in + list order and returns on the first match, so a range that is fully + contained within an earlier, wider one would never be reached -- see + GitHub issue #862, where the source table lists ``0x0101-0x01FF`` + after the ``0x0000-0x05DC`` range that wholly contains it. + + The IANA CSV is not otherwise sorted by range, so this only reorders + the pairs where containment would otherwise shadow one of them; + every other pair keeps the source table's row order, appended in the + order it was read. + + Args: + ranges: Range blocks already placed for emission, as + ``(start, stop, lines)``. + entry: The ``(start, stop, lines)`` tuple to place. + + """ + lo, hi, _ = entry + for index, (rlo, rhi, _) in enumerate(ranges): + if rlo <= lo and hi <= rhi: + ranges.insert(index, entry) + return + ranges.append(entry) + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': """Process CSV data. @@ -93,7 +121,7 @@ def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': next(reader) # header enum = [] # type: list[str] - miss = [] # type: list[str] + ranges = [] # type: list[tuple[int, int, list[str]]] for item in reader: name = item[4] rfcs = item[5] @@ -125,12 +153,14 @@ def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': except ValueError: start, stop = item[1].split('-') - miss.append(f'if 0x{start} <= value <= 0x{stop}:') - miss.append(f' #: {desc}') + block = [f'if 0x{start} <= value <= 0x{stop}:', f' #: {desc}'] if name in self.UNASSIGNED_ROW_NAMES: - miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + block.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") else: - miss.append(f" return extend_enum(cls, '{self.safe_name(name)}_0x%s' % hex(value)[2:].upper().zfill(4), value)") # pylint: disable=line-too-long + block.append(f" return extend_enum(cls, '{self.safe_name(name)}_0x%s' % hex(value)[2:].upper().zfill(4), value)") # pylint: disable=line-too-long + self._insert_range(ranges, (int(start, base=16), int(stop, base=16), block)) + + miss = [line for _, _, block in ranges for line in block] # type: list[str] return enum, miss diff --git a/tests/const/test_const_enum_no_mint.py b/tests/const/test_const_enum_no_mint.py index 6d9c43ae5d..25ac037eaf 100644 --- a/tests/const/test_const_enum_no_mint.py +++ b/tests/const/test_const_enum_no_mint.py @@ -342,23 +342,28 @@ def _first_unassigned_value(cls: 'type') -> 'int': #: generic "first branch that converts, by source order" probe can actually #: exercise -- i.e. the registry's own bounds contain at least one value that #: is not already a declared member, *and* nothing earlier in the same -#: ``_missing_`` masks it. Two different reasons hold registries out of this -#: list: +#: ``_missing_`` masks it. One reason holds registries out of this list: 13 +#: have no reachable gap at all -- the same situation :data:`ALL_REGISTRIES` +#: already documents for :class:`~pcapkit.const.hip.transport.Transport`: +#: every value inside the guard's own bounds names a real member, so +#: ``_missing_`` can never actually run for them (e.g. :class:`~pcapkit.const. +#: ipv4.tos_del.ToSDelay` is bounded to ``0 <= value <= 1`` and both 0 and 1 +#: are declared). They are proved by :class:`RulingConversionSourceTests` (a +#: source sweep) instead. #: -#: * 13 have no reachable gap at all -- the same situation :data:`ALL_ -#: REGISTRIES` already documents for :class:`~pcapkit.const.hip.transport. -#: Transport`: every value inside the guard's own bounds names a real -#: member, so ``_missing_`` can never actually run for them (e.g. :class: -#: `~pcapkit.const.ipv4.tos_del.ToSDelay` is bounded to ``0 <= value <= 1`` -#: and both 0 and 1 are declared). They are proved by :class: -#: `RulingConversionSourceTests` (a source sweep) instead. -#: * :class:`~pcapkit.const.reg.ethertype.EtherType` is excluded for a third -#: reason: its *first* converted branch by source order ("Old Xerox -#: Experimental...", 0x0101-0x01FF) is itself unreachable, masked by the -#: wider 0x0000-0x05DC branch before it -- see :data:`ETHERTYPE_UNASSIGNED_ -#: PROBES`'s comment. A naive probe would silently exercise the wrong -#: branch, so :class:`EtherTypeMixedMintTests` covers it explicitly instead -#: (behaviourally for ``DEC Unassigned``, by source for the masked row). +#: :class:`~pcapkit.const.reg.ethertype.EtherType` used to be held out for a +#: second, distinct reason on top of those 13: its *first* converted branch by +#: source order ("Old Xerox Experimental...", 0x0101-0x01FF) was itself +#: unreachable, masked by the wider 0x0000-0x05DC branch before it, so a naive +#: probe would have silently exercised the wrong branch. GitHub issue #862 +#: (fixed by #865) reordered :meth:`pcapkit.vendor.reg.ethertype.EtherType. +#: process` so the narrower range is tested first, which makes that branch +#: directly probeable like any other reachable-gap registry -- the same shape +#: :class:`~pcapkit.const.ipx.socket.Socket` (also mixed) already demonstrates +#: in this list below. EtherType has therefore joined this set; see +#: :data:`ETHERTYPE_UNASSIGNED_PROBES`'s comment for the probe itself, still +#: covered explicitly by :class:`EtherTypeMixedMintTests` as well since it +#: remains a mixed registry. #: #: Derived the same way as :data:`RULING_CONVERTED_REGISTRIES`: computed once #: by walking each candidate's own bounds for a gap, not hand-picked. @@ -426,6 +431,7 @@ def _first_unassigned_value(cls: 'type') -> 'int': ('pcapkit.const.pcapng.record_type', 'RecordType'), ('pcapkit.const.pcapng.secrets_type', 'SecretsType'), ('pcapkit.const.pcapng.verdict_type', 'VerdictType'), + ('pcapkit.const.reg.ethertype', 'EtherType'), ('pcapkit.const.reg.linktype', 'LinkType'), ('pcapkit.const.reg.transtype', 'TransType'), ('pcapkit.const.tcp.checksum', 'Checksum'), @@ -441,18 +447,26 @@ def _first_unassigned_value(cls: 'type') -> 'int': #: stays a mint; :data:`ETHERTYPE_KEPT_PROBE` pins one (``Xyplex``) as a #: regression guard. #: -#: "Old Xerox Experimental" (0x0101-0x01FF) is not behaviourally probeable: -#: it is a strict subset of the earlier, wider "IEEE802.3 Length Field" branch -#: (0x0000-0x05DC), which the ``_missing_`` if-chain matches first and so -#: masks it completely -- the same shape of pre-existing ordering bug GitHub +#: "Old Xerox Experimental" (0x0101-0x01FF) used to not be behaviourally +#: probeable: it was a strict subset of the earlier, wider "IEEE802.3 Length +#: Field" branch (0x0000-0x05DC), which the ``_missing_`` if-chain matched +#: first and so masked it completely -- the same shape of ordering bug GitHub #: issue #841 found in :mod:`pcapkit.const.ipx.socket`, present on ``main`` -#: before this change and not part of #775/#847's ruling, so it is left as a -#: follow-up rather than reordered here. :class:`EtherTypeMixedMintTests` -#: proves that row by source instead of by calling it. +#: before this change and not part of #775/#847's ruling, so it was left as a +#: follow-up rather than reordered as part of that ruling. GitHub issue #862 +#: is that follow-up, fixed by #865: :meth:`pcapkit.vendor.reg.ethertype. +#: EtherType.process` now tests the narrower Old Xerox range before the wider +#: IEEE802.3 one, so 0x0101 resolves to Old Xerox and is directly probeable -- +#: it is included below alongside ``DEC Unassigned``, giving it the same +#: behavioural no-mint proof :class:`EtherTypeMixedMintTests` already gives +#: DEC Unassigned, rather than the weaker source-text check that stood in for +#: it before #865 (an order-insensitive string search that would keep passing +#: even if the ordering regressed). +ETHERTYPE_OLD_XEROX_LABEL = 'Old_Xerox_Experimental_values_Invalid_as_an_Ethertype_since_1983' ETHERTYPE_UNASSIGNED_PROBES = { 0x8039: 'DEC_Unassigned', + 0x0101: ETHERTYPE_OLD_XEROX_LABEL, } -ETHERTYPE_MASKED_UNASSIGNED_LABEL = 'Old_Xerox_Experimental_values_Invalid_as_an_Ethertype_since_1983' ETHERTYPE_KEPT_PROBE = (0x0888, 'Xyplex_0x0888') #: :class:`~pcapkit.const.ipx.socket.Socket` probes for the owner's ruling: @@ -899,11 +913,16 @@ class RulingConversionSourceTests(unittest.TestCase): REGISTRIES` -- which keep some ``extend_enum`` calls by design.""" def test_no_reachable_gap_registries_no_longer_call_extend_enum(self) -> None: - # EtherType is also absent from RULING_CONVERTED_WITH_REACHABLE_GAP, - # but for the *masked-branch* reason documented on that data, not the - # no-gap-at-all reason this test is about -- and unlike the true 13 it - # still legitimately keeps other extend_enum calls, so it is excluded - # from this set too and covered by EtherTypeMixedMintTests instead. + # EtherType now belongs to RULING_CONVERTED_WITH_REACHABLE_GAP: its + # masked-branch exclusion (Old Xerox shadowed by the wider IEEE802.3 + # range) was fixed by GitHub issue #862/#865, and its first converted + # branch by source order is directly probeable like any other + # reachable-gap registry now. The `_MIXED_REGISTRIES` subtraction + # below is a defensive no-op today -- neither mixed registry needs + # removing from `RCR - RG` any more (EtherType is already in RG; + # Socket never appeared in RULING_CONVERTED_REGISTRIES to begin with) + # -- kept so a mixed registry that regains a masked or absent branch + # in the future does not silently inflate this count. excluded = set(RULING_CONVERTED_REGISTRIES) - set(RULING_CONVERTED_WITH_REACHABLE_GAP) - _MIXED_REGISTRIES self.assertEqual(len(excluded), 13, 'expected exactly 13 registries with no reachable gap') for module_name, class_name in sorted(excluded): @@ -949,16 +968,19 @@ def test_unassigned_rows_do_not_mint(self) -> None: self.assertEqual(before, len(EtherType.__members__)) self.assertNotIn(value, EtherType._value2member_map_) # type: ignore[attr-defined] - def test_masked_old_xerox_row_converts_by_source(self) -> None: - """0x0101-0x01FF is unreachable at runtime -- masked by the wider - 0x0000-0x05DC branch that precedes it, a pre-existing ordering issue - (see :data:`ETHERTYPE_UNASSIGNED_PROBES`'s comment) unrelated to this - ruling. Proved by source instead of by calling it.""" - from pcapkit.const.reg.ethertype import EtherType - - source = inspect.getsource(EtherType._missing_) # type: ignore[attr-defined] - self.assertIn(f"cls._unregistered_member(value, '{ETHERTYPE_MASKED_UNASSIGNED_LABEL}')", source) - self.assertNotIn(f"extend_enum(cls, '{ETHERTYPE_MASKED_UNASSIGNED_LABEL}", source) + # ``test_masked_old_xerox_row_converts_by_source`` retired here, not simply + # deleted: it used to prove 0x0101-0x01FF converts *by source* (an + # order-insensitive string search for the two calls in + # EtherType._missing_'s text) because the row was unreachable at runtime, + # masked by the wider 0x0000-0x05DC branch that preceded it. GitHub issue + # #862, fixed by #865, reordered the generator so that row is no longer + # masked, and 0x0101 is now one of the values ``test_unassigned_rows_do_ + # not_mint`` above probes directly via ``ETHERTYPE_UNASSIGNED_PROBES`` -- + # a strictly stronger, behavioural proof of the same fact (it also catches + # a regression the old source check would have missed: the string search + # never checked *order*, so it would keep passing even if the ordering + # regressed and 0x0101 started minting again). Nothing this test checked + # is left unchecked; it is subsumed rather than replaced by a weaker test. def test_attributed_vendor_block_still_mints(self) -> None: from pcapkit.const.reg.ethertype import EtherType diff --git a/tests/const/test_const_ethertype_862_unit.py b/tests/const/test_const_ethertype_862_unit.py new file mode 100644 index 0000000000..31af665f82 --- /dev/null +++ b/tests/const/test_const_ethertype_862_unit.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +"""``EtherType(0x0101)`` must resolve to Old Xerox, not IEEE802.3 Length Field. + +GitHub issue #862: :meth:`pcapkit.const.reg.ethertype.EtherType._missing_` tested +``0x0000 <= value <= 0x05DC`` (IEEE802.3 Length Field) *before* +``0x0101 <= value <= 0x01FF`` (Old Xerox Experimental values). The second range is +wholly contained in the first, and the generated ``_missing_`` returns on the +first matching ``if``, so the Old Xerox branch was unreachable for every value +it covers -- ``EtherType(0x0101).name`` resolved to +``'IEEE802_3_Length_Field_0x0101'`` instead of the Old Xerox name. + +Same shape as #844/#852 (``LinkType`` 209) and #841 (IPX socket ranges): the +generated order follows the source table's own row order, and IANA's +``ieee-802-numbers-1.csv`` lists the wide IEEE802.3 range (row 2) ahead of the +narrower Old Xerox range (row 3) that it wholly contains. + +Unlike #841's IPX socket table, the range bounds here are not literals in the +crawler -- :mod:`pcapkit.vendor.reg.ethertype` fetches the live IANA CSV on +every regeneration, so there is no local data table to hand-reorder. The fix +is therefore general rather than a special case for these two constants: +:meth:`~pcapkit.vendor.reg.ethertype.EtherType.process` now collects every +range row's ``(start, stop, body)`` before rendering, and +:meth:`~pcapkit.vendor.reg.ethertype.EtherType._insert_range` places each new +range immediately ahead of the first already-placed range that fully contains +it, leaving every non-overlapping pair in the CSV's own row order. Sweeping +all 56 range tests in the committed, generated file (measured directly against +``main`` at ``70fa92010``, the #861 merge commit this issue was blocked on) +found this to be the *only* containment pair among them -- no other subsumed +range exists in :class:`~pcapkit.const.reg.ethertype.EtherType` today, so +:class:`EtherTypeGeneratorGeneralOrderingTests` below pins the algorithm +itself against synthetic nested ranges rather than a second real-world pair. + +This module pins both the symptom and the root cause: + +- :class:`EtherType862ConstResolutionTests` -- against the committed, generated + :class:`pcapkit.const.reg.ethertype.EtherType`, the same lookup the issue + reported, plus the neighbouring values the fix must leave alone. +- :class:`EtherTypeGeneratorRangeOrderingTests` -- against + :meth:`~pcapkit.vendor.reg.ethertype.EtherType.process` directly, fed a CSV + fixture reproducing IANA's own rows for these two ranges plus one unrelated, + non-overlapping range, byte-for-byte (captured from a live fetch of + ``https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers-1.csv`` + on 2026-09-27). Needs no network: the fixture *is* the table rows, not a + fetch of them, so this stays reliable in CI while still exercising the exact + code path a real crawl runs. A regeneration that drops the fix reintroduces + the wrong order here before it ever reaches the committed const file. +- :class:`EtherTypeGeneratorGeneralOrderingTests` -- pins + :meth:`~pcapkit.vendor.reg.ethertype.EtherType._insert_range` itself against + three synthetic nested ranges, so the ordering rule is proven general rather + than only verified against the one pair #862 happened to report. + +""" +from __future__ import annotations + +import unittest + +from tests._support import purge_modules + +#: CSV fixture reproducing IANA's own rows for the #862 pair plus one +#: unrelated, non-overlapping range, byte-for-byte (captured from a live fetch +#: of ``https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers-1.csv`` +#: on 2026-09-27). Row order matches the live table: the wide IEEE802.3 range +#: first, the narrower Old Xerox range it contains second, then the +#: unrelated Xyplex range -- exactly the order that reproduces the defect +#: against unfixed code. +ETHERTYPE_862_FIXTURE_CSV = [ + 'Ethertype (decimal),Ethertype (hex),Exp. Ethernet (decimal),Exp. Ethernet (octal),Description,Reference', + '0000-1500,0000-05DC,-,-,IEEE802.3 Length Field,[IEEE Std 802.3]', + '0257-0511,0101-01FF,-,-,Old Xerox Experimental values. Invalid as an Ethertype since 1983.,[Neil_Sembower]', + '2184-2186,0888-088A,-,-,Xyplex,[Neil_Sembower]', +] + + +class EtherType862ConstResolutionTests(unittest.TestCase): + """Against the generated, committed :class:`~pcapkit.const.reg.ethertype.EtherType`.""" + + def setUp(self) -> None: + purge_modules(['pcapkit']) + + def test_value_0x0101_resolves_to_old_xerox_name(self) -> None: + from pcapkit.const.reg.ethertype import EtherType + + self.assertEqual( + EtherType(0x0101).name, + 'Old_Xerox_Experimental_values_Invalid_as_an_Ethertype_since_1983', + "EtherType(0x0101).name resolved to the IEEE802.3 Length Field " + "name instead of the Old Xerox one; see GitHub issue #862", + ) + + def test_value_0x01ff_the_top_of_the_range_also_resolves_to_old_xerox(self) -> None: + from pcapkit.const.reg.ethertype import EtherType + + self.assertEqual( + EtherType(0x01FF).name, + 'Old_Xerox_Experimental_values_Invalid_as_an_Ethertype_since_1983', + ) + + def test_ieee_802_3_values_outside_the_old_xerox_range_are_unaffected(self) -> None: + # Nothing else in the IEEE802.3 range moved: values below, and above, + # the Old Xerox sub-range still resolve to IEEE802.3 Length Field. + from pcapkit.const.reg.ethertype import EtherType + + for value in (0x0000, 0x0050, 0x0300, 0x05DC): + with self.subTest(value=hex(value)): + self.assertEqual( + EtherType(value).name, + 'IEEE802_3_Length_Field_0x%04X' % value, + ) + + def test_xerox_ns_idp_a_directly_defined_member_is_unaffected(self) -> None: + # 0x0600 is a real, directly-defined enum member (not a _missing_ + # range at all); the reordering must not disturb ordinary lookups. + from pcapkit.const.reg.ethertype import EtherType + + self.assertEqual(EtherType(0x0600).name, 'XEROX_NS_IDP') + + +class EtherTypeGeneratorRangeOrderingTests(unittest.TestCase): + """Against the generator's own ``process()``, root cause rather than symptom.""" + + def setUp(self) -> None: + purge_modules(['pcapkit']) + + @staticmethod + def _process_fixture() -> 'tuple[list[str], list[str]]': + """Run :meth:`EtherType.process` over the #862 fixture rows. + + Builds the instance with ``object.__new__`` rather than calling + ``EtherType()`` directly: the real constructor + (:meth:`pcapkit.vendor.default.Vendor.__init__`) fetches the live + registry and writes the generated const file as a side effect, and + this test needs neither -- only the pure :meth:`process` step that + turns registry rows into rendered ``_missing_`` branches. + """ + from pcapkit.vendor.reg.ethertype import EtherType + + inst = object.__new__(EtherType) + enum, miss = inst.process(ETHERTYPE_862_FIXTURE_CSV) + return enum, miss + + def test_old_xerox_range_is_emitted_before_ieee_802_3(self) -> None: + _enum, miss = self._process_fixture() + text = '\n'.join(miss) + + xerox_pos = text.index('if 0x0101 <= value <= 0x01FF:') + ieee_pos = text.index('if 0x0000 <= value <= 0x05DC:') + + self.assertLess( + xerox_pos, ieee_pos, + f'the narrower Old Xerox range must be tested before the wider ' + f'IEEE802.3 range that contains it, or it is never reached -- ' + f'see GitHub issue #862; rendered miss block:\n{text}' + ) + + def test_unrelated_non_overlapping_range_keeps_table_order(self) -> None: + # Xyplex neither contains nor is contained by either #862 range, so + # reordering the #862 pair must not move it: it stays after both, + # exactly the CSV's own row order. + _enum, miss = self._process_fixture() + text = '\n'.join(miss) + + ieee_pos = text.index('if 0x0000 <= value <= 0x05DC:') + xyplex_pos = text.index('if 0x0888 <= value <= 0x088A:') + + self.assertLess(ieee_pos, xyplex_pos) + + def test_both_ranges_still_render_with_their_own_body(self) -> None: + # Reordering must not lose either range or swap their bodies. + _enum, miss = self._process_fixture() + text = '\n'.join(miss) + + self.assertIn( + "return cls._unregistered_member(value, " + "'Old_Xerox_Experimental_values_Invalid_as_an_Ethertype_since_1983')", + text, + ) + self.assertIn( + "return extend_enum(cls, 'IEEE802_3_Length_Field_0x%s' " + "% hex(value)[2:].upper().zfill(4), value)", + text, + ) + + +class EtherTypeGeneratorGeneralOrderingTests(unittest.TestCase): + """Pins :meth:`~pcapkit.vendor.reg.ethertype.EtherType._insert_range` itself. + + #862's own pair is the only containment among the 56 ranges in today's + committed file, so this exercises the general rule -- narrower range + before any wider range that contains it -- against synthetic nested + ranges the live registry does not (yet) happen to contain. + """ + + def setUp(self) -> None: + purge_modules(['pcapkit']) + + def test_nested_ranges_are_ordered_narrowest_first(self) -> None: + """Regression pin against two specific wrong implementations of + "insert ahead of a containing range" -- not a general correctness + proof for arbitrary nesting depth or arrival order. That exhaustive + proof (every arrival order at 3 and 4 levels, plus randomised + fuzzing) was done separately during GitHub issue #862/#865's review + and does not live in this repository as a test; this method only + pins the two specific shortcuts that review considered and rejected. + + Against these three nested ranges (OUTER contains MIDDLE contains + INNER), the correct result is always ``[INNER, MIDDLE, OUTER]``, but + no *single* arrival order of the six possible ones catches both of + the following wrong implementations at once -- verified by running + both against the real :meth:`~pcapkit.vendor.reg.ethertype.EtherType. + _insert_range` across all six orderings: + + * ``naiveA`` -- "insert at index 0 whenever contained by anything" -- + diverges from the real result only on arrival orders ``INNER, + OUTER, MIDDLE`` and ``OUTER, INNER, MIDDLE`` (both produce + ``[MIDDLE, INNER, OUTER]`` instead). + * ``naiveB`` -- "insert immediately before the *last* containing + range" -- diverges only on ``OUTER, MIDDLE, INNER`` and ``MIDDLE, + OUTER, INNER`` (both produce ``[MIDDLE, INNER, OUTER]`` instead). + * The other two orderings, ``MIDDLE, INNER, OUTER`` and ``INNER, + MIDDLE, OUTER``, catch neither -- all three implementations agree + on them, so they are not exercised below. + + So this test drives two orders, one representative of each + discriminating pair, rather than one order claimed (wrongly, in an + earlier revision) to catch both. + """ + from pcapkit.vendor.reg.ethertype import EtherType + + outer = (0x0000, 0xFFFF, ['OUTER']) + middle = (0x1000, 0x2000, ['MIDDLE']) + inner = (0x1500, 0x1600, ['INNER']) + + orderings = ( + ('INNER, OUTER, MIDDLE -- catches the naiveA "insert at front" mistake', (inner, outer, middle)), + ('OUTER, MIDDLE, INNER -- catches the naiveB "insert before the last container" mistake', (outer, middle, inner)), + ) + for label, order in orderings: + with self.subTest(order=label): + ranges = [] # type: list[tuple[int, int, list[str]]] + for entry in order: + EtherType._insert_range(ranges, entry) + + self.assertEqual( + [block[0] for _, _, block in ranges], + ['INNER', 'MIDDLE', 'OUTER'], + ) + + def test_disjoint_ranges_keep_insertion_order(self) -> None: + from pcapkit.vendor.reg.ethertype import EtherType + + ranges = [] # type: list[tuple[int, int, list[str]]] + EtherType._insert_range(ranges, (0x0000, 0x00FF, ['FIRST'])) + EtherType._insert_range(ranges, (0x0200, 0x02FF, ['SECOND'])) + EtherType._insert_range(ranges, (0x0400, 0x04FF, ['THIRD'])) + + self.assertEqual( + [block[0] for _, _, block in ranges], + ['FIRST', 'SECOND', 'THIRD'], + ) + + +if __name__ == '__main__': + unittest.main()