proj/portage:master commit in: lib/portage/tests/dep/

"Matt Turner" <[email protected]>
Newsgroups gmane.linux.gentoo.cvs
Message-ID <1785963928.06da2f580686e115ce2b278da31f62512f8dc37f.mattst88@gentoo>
commit:     06da2f580686e115ce2b278da31f62512f8dc37f
Author:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
AuthorDate: Sun Aug  2 20:18:11 2026 +0000
Commit:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
CommitDate: Wed Aug  5 21:05:28 2026 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=06da2f58

tests: add C parser parity and classify_use_deps tests

_UseReduceTests is a mixin that exercises use_reduce() with
token_class=Atom; TestUseReducePythonPath and TestUseReduceCPath run it
with the C parser disabled and enabled respectively, so every
correctness assertion runs on both code paths.

- TestCParserRawAtom checks the raw _parser.Atom field values.
- TestCAtomObjectProtocol covers _parser.Atom's str/repr/hash/equality,
  including comparison against a foreign type, and that ordering is not
  implemented.
- TestClassifyUseDeps unit-tests _parser.classify_use_deps() for each
  token form (enabled, disabled, conditional, missing-default).
- TestCFastVsPython asserts byte-identical output between paths on a
  representative set of dep strings and confirms both raise
  InvalidDependString on the same malformed inputs.
- TestCParserMalformedGroups covers the group syntax errors that each
  have their own path in scan_item()/scan_group_contents(): missing
  whitespace after '(' or after a "||"/"flag?" prefix, and unterminated
  groups.
- TestCParserInactiveConditionalBodies covers the skip visitor, which
  parses the body of an inactive use conditional without emitting it, so
  nested groups and nested conditionals are still validated and syntax
  errors inside a body that is never emitted are still reported.
- TestCParserWhitespace covers leading, trailing and repeated
  whitespace, which the C parser handles itself because it sees the dep
  string unsplit.
- TestCParserEapiGuard covers eapi in {None,0,1,4,5,6,7,8}: older EAPIs
  reject modern syntax through both paths, and valid strings produce
  identical results.
- TestKillSwitch runs a subprocess per case, since
  PORTAGE_NATIVE_DEP_PARSER is consulted when portage.dep is imported.
- TestLongAtoms covers category and package names either side of the
  point where the C path stops using a stack buffer to join cp and cpv.
- TestDeepNesting checks that the C parser's heap-grown group stack
  imposes no nesting limit the pure-Python path lacks, across the
  boundary where that stack spills to the heap.

Every _c_dep_parser toggle goes through the _use_c_parser() context
manager, so the invariant -- a toggle never outlives a single call, and
a failing assertion cannot leak it into the next test -- is stated in
one place. pytest-xdist distributes over processes rather than threads,
so each worker has its own copy of portage.dep and cannot race.

Signed-off-by: Matt Turner <mattst88 <AT> gentoo.org>

 lib/portage/tests/dep/meson.build      |    1 +
 lib/portage/tests/dep/test_c_parser.py | 1072 ++++++++++++++++++++++++++++++++
 2 files changed, 1073 insertions(+)

diff --git a/lib/portage/tests/dep/meson.build b/lib/portage/tests/dep/meson.build
index 7350f7775..e438c88d0 100644
--- a/lib/portage/tests/dep/meson.build
+++ b/lib/portage/tests/dep/meson.build
@@ -2,6 +2,7 @@ py.install_sources(
     [
         'test_atom.py',
         'test_check_required_use.py',
+        'test_c_parser.py',
         'test_extended_atom_dict.py',
         'test_extract_affecting_use.py',
         'test_standalone.py',

diff --git a/lib/portage/tests/dep/test_c_parser.py b/lib/portage/tests/dep/test_c_parser.py
new file mode 100644
index 000000000..d845f4367
--- /dev/null
+++ b/lib/portage/tests/dep/test_c_parser.py
@@ -0,0 +1,1072 @@
+# Copyright 2026 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+import contextlib
+import operator
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+import portage
+import portage.dep as _dep_mod
+from portage.dep import Atom, _get_eapi_attrs, _use_dep, use_reduce
+from portage.exception import InvalidDependString
+from portage.tests import TestCase
+
+_orig_c_dep_parser = _dep_mod._c_dep_parser
+
+
[email protected]
+def _use_c_parser(enabled):
+    """Select the parser used by portage.dep for the duration of the block.
+
+    portage.dep._c_dep_parser is module state, so a test may not leave it
+    toggled and two tests may not toggle it concurrently in one interpreter.
+    Every toggle in this file goes through here, and always around a single
+    call, so a failing assertion cannot leak the setting into the next test.
+    pytest-xdist distributes over processes rather than threads, so each
+    worker has its own copy of the module and cannot race with another."""
+    _dep_mod._c_dep_parser = _orig_c_dep_parser if enabled else None
+    try:
+        yield
+    finally:
+        _dep_mod._c_dep_parser = _orig_c_dep_parser
+
+
+def _c_parser():
+    try:
+        from portage.dep import _parser
+
+        return _parser
+    except ImportError:
+        return None
+
+
+def _atoms_equal(a, b):
+    for f in (
+        "_string",
+        "_cp",
+        "_cpv",
+        "_version",
+        "_operator",
+        "_slot",
+        "_sub_slot",
+        "_slot_operator",
+        "_eapi",
+        "_extended_syntax",
+        "_build_id",
+    ):
+        av, bv = getattr(a, f), getattr(b, f)
+        if av != bv:
+            return False, f"{f}: {av!r} != {bv!r}"
+
+    ab, bb = a._blocker_obj, b._blocker_obj
+    if bool(ab) != bool(bb):
+        return False, f"_blocker_obj presence: {ab!r} != {bb!r}"
+    if ab and bb and ab.overlap.forbid != bb.overlap.forbid:
+        return False, "_blocker_obj.overlap.forbid mismatch"
+
+    au, bu = a._use, b._use
+    if (au is None) != (bu is None):
+        return False, f"_use presence: {au!r} != {bu!r}"
+    if au is not None and bu is not None:
+        for attr in (
+            "tokens",
+            "enabled",
+            "disabled",
+            "required",
+            "missing_enabled",
+            "missing_disabled",
+        ):
+            av2, bv2 = getattr(au, attr), getattr(bu, attr)
+            if av2 != bv2:
+                return False, f"_use.{attr}: {av2!r} != {bv2!r}"
+        ac, bc = au.conditional, bu.conditional
+        if (ac is None) != (bc is None):
+            return False, f"_use.conditional presence: {ac!r} != {bc!r}"
+        if ac is not None and bc is not None:
+            for k in ("enabled", "disabled", "equal", "not_equal"):
+                av2, bv2 = getattr(ac, k), getattr(bc, k)
+                if av2 != bv2:
+                    return False, f"_use.conditional.{k}: {av2!r} != {bv2!r}"
+    return True, ""
+
+
+def _result_equal(c_result, py_result):
+    if len(c_result) != len(py_result):
+        return False, f"length {len(c_result)} != {len(py_result)}"
+    for i, (ci, pi) in enumerate(zip(c_result, py_result)):
+        if isinstance(ci, list) and isinstance(pi, list):
+            ok, msg = _result_equal(ci, pi)
+            if not ok:
+                return False, f"[{i}]: {msg}"
+        elif isinstance(ci, str) and isinstance(pi, str):
+            if ci != pi:
+                return False, f"[{i}]: {ci!r} != {pi!r}"
+        elif isinstance(ci, Atom) and isinstance(pi, Atom):
+            ok, msg = _atoms_equal(ci, pi)
+            if not ok:
+                return False, f"[{i}] Atom mismatch: {msg}"
+        else:
+            return False, f"[{i}]: type mismatch {type(ci)} vs {type(pi)}"
+    return True, ""
+
+
+class _UseReduceTests:
+    """Mixin run with USE_C_PARSER True or False; subclasses set it."""
+
+    USE_C_PARSER = False
+
+    def setUp(self):
+        _dep_mod._c_dep_parser = _orig_c_dep_parser if self.USE_C_PARSER else None
+
+    def tearDown(self):
+        _dep_mod._c_dep_parser = _orig_c_dep_parser
+
+    def _reduce(self, depstr, uselist=None, matchall=False, eapi="8", **kw):
+        return use_reduce(
+            depstr,
+            uselist=uselist or [],
+            matchall=matchall,
+            token_class=Atom,
+            eapi=eapi,
+            **kw,
+        )
+
+    def _atom(self, depstr, **kw):
+        result = self._reduce(depstr, **kw)
+        self.assertEqual(len(result), 1)
+        self.assertIsInstance(result[0], Atom)
+        return result[0]
+
+    def test_unversioned_atom_fields(self):
+        a = self._atom("dev-libs/foo", matchall=True)
+        self.assertEqual(a._cp, "dev-libs/foo")
+        self.assertEqual(a._cpv, "dev-libs/foo")
+        self.assertIsNone(a._version)
+        self.assertIsNone(a._operator)
+        self.assertIsNone(a._slot)
+        self.assertIsNone(a._sub_slot)
+        self.assertIsNone(a._slot_operator)
+        self.assertIsNone(a._use)
+        self.assertIsNone(a._blocker_obj)
+
+    def test_versioned_atom_fields(self):
+        a = self._atom("=dev-libs/foo-1.2.3-r1", matchall=True)
+        self.assertEqual(a._cp, "dev-libs/foo")
+        self.assertEqual(a._version, "1.2.3-r1")
+        self.assertEqual(a._operator, "=")
+        self.assertEqual(a._cpv, "dev-libs/foo-1.2.3-r1")
+
+    def test_all_operators(self):
+        for op in ("=", ">=", ">", "<=", "<", "~"):
+            with self.subTest(op=op):
+                a = self._atom(f"{op}cat/pkg-1.0", matchall=True)
+                self.assertEqual(a._operator, op)
+                self.assertEqual(a._version, "1.0")
+
+    def test_version_suffixes(self):
+        for ver in ("1.0_alpha1", "1.0_beta2", "1.0_pre1", "1.0_rc1", "1.0_p1"):
+            with self.subTest(ver=ver):
+                a = self._atom(f"=cat/pkg-{ver}", matchall=True)
+                self.assertEqual(a._version, ver)
+
+    def test_slot_fields(self):
+        a = self._atom("dev-libs/foo:1", matchall=True)
+        self.assertEqual(a._slot, "1")
+        self.assertIsNone(a._sub_slot)
+        self.assertIsNone(a._slot_operator)
+
+    def test_sub_slot_fields(self):
+        a = self._atom("dev-libs/foo:0/53", matchall=True)
+        self.assertEqual(a._slot, "0")
+        self.assertEqual(a._sub_slot, "53")
+        self.assertIsNone(a._slot_operator)
+
+    def test_slot_operator_eq(self):
+        a = self._atom("dev-libs/foo:0=", matchall=True)
+        self.assertEqual(a._slot, "0")
+        self.assertEqual(a._slot_operator, "=")
+
+    def test_slot_operator_star(self):
+        a = self._atom("dev-libs/foo:*", matchall=True)
+        self.assertIsNone(a._slot)
+        self.assertEqual(a._slot_operator, "*")
+
+    def test_slot_operator_bare_eq(self):
+        a = self._atom("dev-libs/foo:=", matchall=True)
+        self.assertIsNone(a._slot)
+        self.assertEqual(a._slot_operator, "=")
+
+    def test_blocker_weak(self):
+        a = self._atom("!dev-libs/foo", matchall=True)
+        self.assertIsNotNone(a._blocker_obj)
+        self.assertFalse(a._blocker_obj.overlap.forbid)
+
+    def test_blocker_strong(self):
+        a = self._atom("!!dev-libs/foo", matchall=True)
+        self.assertIsNotNone(a._blocker_obj)
+        self.assertTrue(a._blocker_obj.overlap.forbid)
+
+    def test_combined_fields(self):
+        a = self._atom(
+            "=sys-apps/portage-2.1-r1:0[doc,a=,!b=,c?,!d?,-e]",
+            matchall=True,
+        )
+        self.assertEqual(a._cp, "sys-apps/portage")
+        self.assertEqual(a._version, "2.1-r1")
+        self.assertEqual(a._operator, "=")
+        self.assertEqual(a._slot, "0")
+        self.assertIsNotNone(a._use)
+        self.assertEqual(a._use.tokens, ("doc", "a=", "!b=", "c?", "!d?", "-e"))
+
+    def test_use_enabled(self):
+        a = self._atom("dev-libs/foo[bar]", matchall=True)
+        self.assertIsNotNone(a._use)
+        self.assertIn("bar", a._use.enabled)
+        self.assertEqual(a._use.disabled, frozenset())
+
+    def test_use_disabled(self):
+        a = self._atom("dev-libs/foo[-bar]", matchall=True)
+        self.assertIn("bar", a._use.disabled)
+        self.assertEqual(a._use.enabled, frozenset())
+
+    def test_use_conditional_enabled(self):
+        a = self._atom("dev-libs/foo[bar?]", matchall=True)
+        self.assertIsNotNone(a._use.conditional)
+        self.assertIn("bar", a._use.conditional.enabled)
+
+    def test_use_conditional_disabled(self):
+        a = self._atom("dev-libs/foo[!bar?]", matchall=True)
+        self.assertIsNotNone(a._use.conditional)
+        self.assertIn("bar", a._use.conditional.disabled)
+
+    def test_use_equal(self):
+        a = self._atom("dev-libs/foo[bar=]", matchall=True)
+        self.assertIn("bar", a._use.conditional.equal)
+
+    def test_use_not_equal(self):
+        a = self._atom("dev-libs/foo[!bar=]", matchall=True)
+        self.assertIn("bar", a._use.conditional.not_equal)
+
+    def test_use_missing_enabled_default(self):
+        a = self._atom("dev-libs/foo[bar(+)]", matchall=True)
+        self.assertIn("bar", a._use.missing_enabled)
+
+    def test_use_missing_disabled_default(self):
+        a = self._atom("dev-libs/foo[bar(-)]", matchall=True)
+        self.assertIn("bar", a._use.missing_disabled)
+
+    def test_use_str(self):
+        a = self._atom("dev-libs/foo[bar,-baz]", matchall=True)
+        self.assertEqual(str(a._use), "[bar,-baz]")
+
+    def test_conditional_active(self):
+        a = self._atom("dev-libs/foo[bar?]", uselist=["bar"])
+        self.assertIsNone(a._use.conditional)
+        self.assertIn("bar", a._use.enabled)
+
+    def test_conditional_inactive(self):
+        a = self._atom("dev-libs/foo[bar?]", uselist=[])
+        self.assertIsNone(a._use)
+
+    def test_conditional_not_equal_active(self):
+        a = self._atom("dev-libs/foo[!bar=]", uselist=["bar"])
+        self.assertIsNone(a._use.conditional)
+        self.assertIn("bar", a._use.disabled)
+
+    def test_conditional_not_equal_inactive(self):
+        a = self._atom("dev-libs/foo[!bar=]", uselist=[])
+        self.assertIsNone(a._use.conditional)
+        self.assertIn("bar", a._use.enabled)
+
+    def test_or_group(self):
+        result = self._reduce("|| ( dev-libs/a dev-libs/b )", matchall=True)
+        self.assertEqual(result[0], "||")
+        self.assertIsInstance(result[1], list)
+        self.assertEqual(len(result[1]), 2)
+
+    def test_use_conditional_group_active(self):
+        result = self._reduce("foo? ( dev-libs/a dev-libs/b )", uselist=["foo"])
+        atoms = [x for x in result if isinstance(x, Atom)]
+        self.assertEqual(len(atoms), 2)
+
+    def test_use_conditional_group_inactive(self):
+        result = self._reduce("foo? ( dev-libs/a dev-libs/b )", uselist=[])
+        self.assertEqual(result, [])
+
+    def test_nested_groups(self):
+        result = self._reduce(
+            "a? ( || ( dev-libs/a1 dev-libs/a2 ) b? ( dev-libs/b ) )",
+            uselist=["a", "b"],
+        )
+        self.assertIn("||", result)
+
+    def test_matchall_expands_all(self):
+        result = self._reduce("a? ( dev-libs/A ) !b? ( dev-libs/B )", matchall=True)
+        cps = [a._cp for a in result if isinstance(a, Atom)]
+        self.assertIn("dev-libs/A", cps)
+        self.assertIn("dev-libs/B", cps)
+
+    def test_glob_version(self):
+        a = self._atom("=dev-libs/foo-1.2*", matchall=True)
+        self.assertEqual(a._cp, "dev-libs/foo")
+        self.assertEqual(a._operator, "=*")
+        self.assertIn("1.2", a._version)
+
+    def test_complex_depstr(self):
+        result = self._reduce(
+            "dev-libs/A >=dev-libs/B-2.0 || ( dev-libs/C dev-libs/D ) "
+            "foo? ( =dev-libs/E-1.0:0[bar,baz?] )",
+            uselist=["foo"],
+        )
+        cps = [a._cp for a in result if isinstance(a, Atom)]
+        self.assertIn("dev-libs/A", cps)
+        self.assertIn("dev-libs/B", cps)
+        self.assertIn("dev-libs/E", cps)
+        self.assertEqual(result[2], "||")
+        self.assertIsInstance(result[3], list)
+        or_cps = [a._cp for a in result[3] if isinstance(a, Atom)]
+        self.assertIn("dev-libs/C", or_cps)
+        self.assertIn("dev-libs/D", or_cps)
+
+    def test_opconvert(self):
+        result = use_reduce(
+            "|| ( dev-libs/a dev-libs/b )",
+            token_class=Atom,
+            matchall=True,
+            eapi="8",
+            opconvert=True,
+        )
+        self.assertIsInstance(result, list)
+
+    def test_flat(self):
+        result = use_reduce(
+            "a? ( dev-libs/a ) dev-libs/b",
+            token_class=Atom,
+            matchall=True,
+            eapi="8",
+            flat=True,
+        )
+        self.assertIsInstance(result, list)
+
+    def test_no_token_class(self):
+        result = use_reduce("dev-libs/a dev-libs/b", matchall=True, eapi="8")
+        self.assertTrue(all(isinstance(x, str) for x in result))
+
+    def test_invalid_missing_close_paren(self):
+        with self.assertRaises(InvalidDependString):
+            self._reduce("|| ( dev-libs/a dev-libs/b", matchall=True)
+
+    def test_invalid_extra_close_paren(self):
+        with self.assertRaises(InvalidDependString):
+            self._reduce("dev-libs/a )", matchall=True)
+
+    def test_invalid_atom_no_category(self):
+        with self.assertRaises(InvalidDependString):
+            self._reduce("foo", matchall=True)
+
+    def test_invalid_operator_no_version(self):
+        with self.assertRaises(InvalidDependString):
+            self._reduce(">=dev-libs/foo", matchall=True)
+
+    def test_eapi5(self):
+        a = self._atom("dev-libs/foo[bar=]", matchall=True, eapi="5")
+        self.assertIn("bar", a._use.conditional.equal)
+
+    def test_eapi6(self):
+        a = self._atom("dev-libs/foo:0/1=[bar,!baz?]", matchall=True, eapi="6")
+        self.assertEqual(a._slot, "0")
+        self.assertEqual(a._sub_slot, "1")
+
+
+class TestUseReducePythonPath(_UseReduceTests, TestCase):
+    USE_C_PARSER = False
+
+
+class TestUseReduceCPath(_UseReduceTests, TestCase):
+    USE_C_PARSER = True
+
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+        super().setUp()
+
+
+class TestCParserRawAtom(TestCase):
+    def setUp(self):
+        self._parser = _c_parser()
+        if self._parser is None:
+            self.skipTest("_parser extension not available")
+
+    def test_parse_returns_list(self):
+        result = self._parser.parse("cat/pkg", matchall=True)
+        self.assertIsInstance(result, list)
+        self.assertEqual(len(result), 1)
+        self.assertIsInstance(result[0], self._parser.Atom)
+
+    def test_raw_atom_fields(self):
+        a = self._parser.parse(
+            "=sys-apps/portage-2.1-r1:0/1=[foo,-bar]", matchall=True
+        )[0]
+        self.assertEqual(a.cp, "sys-apps/portage")
+        self.assertEqual(a.version, "2.1-r1")
+        self.assertEqual(a.operator, "=")
+        self.assertEqual(a.slot, "0")
+        self.assertEqual(a.sub_slot, "1")
+        self.assertEqual(a.slot_operator, "=")
+        self.assertEqual(tuple(a.use), ("foo", "-bar"))
+        self.assertIsNone(a.blocker)
+
+    def test_raw_atom_blocker(self):
+        a = self._parser.parse("!!dev-libs/foo", matchall=True)[0]
+        self.assertEqual(a.blocker, "!!")
+        a2 = self._parser.parse("!dev-libs/foo", matchall=True)[0]
+        self.assertEqual(a2.blocker, "!")
+
+    def test_raw_atom_glob_version(self):
+        a = self._parser.parse("=dev-libs/foo-1.2*", matchall=True)[0]
+        self.assertEqual(a.cp, "dev-libs/foo")
+        self.assertEqual(a.operator, "=")  # _c_atom_from_c converts this to "=*"
+        self.assertEqual(a.version, "1.2*")
+
+
+class TestClassifyUseDeps(TestCase):
+    def setUp(self):
+        self._parser = _c_parser()
+        if self._parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _classify(self, tokens):
+        return self._parser.classify_use_deps(tokens)
+
+    def test_enabled(self):
+        en, dis, _, _, cond, req = self._classify(("foo",))
+        self.assertEqual(en, frozenset({"foo"}))
+        self.assertEqual(dis, frozenset())
+        self.assertIsNone(cond)
+        self.assertEqual(req, frozenset({"foo"}))
+
+    def test_disabled(self):
+        en, dis, _, _, cond, req = self._classify(("-foo",))
+        self.assertEqual(en, frozenset())
+        self.assertEqual(dis, frozenset({"foo"}))
+        self.assertIsNone(cond)
+        self.assertEqual(req, frozenset({"foo"}))
+
+    def test_conditional_enabled(self):
+        _, _, _, _, cond, _ = self._classify(("foo?",))
+        self.assertIsNotNone(cond)
+        self.assertEqual(cond["enabled"], frozenset({"foo"}))
+
+    def test_conditional_disabled(self):
+        _, _, _, _, cond, _ = self._classify(("!foo?",))
+        self.assertIsNotNone(cond)
+        self.assertEqual(cond["disabled"], frozenset({"foo"}))
+
+    def test_conditional_equal(self):
+        _, _, _, _, cond, _ = self._classify(("foo=",))
+        self.assertIsNotNone(cond)
+        self.assertEqual(cond["equal"], frozenset({"foo"}))
+
+    def test_conditional_not_equal(self):
+        _, _, _, _, cond, _ = self._classify(("!foo=",))
+        self.assertIsNotNone(cond)
+        self.assertEqual(cond["not_equal"], frozenset({"foo"}))
+
+    def test_missing_enabled_default(self):
+        en, _, miss_en, miss_dis, _, req = self._classify(("foo(+)",))
+        self.assertEqual(en, frozenset({"foo"}))
+        self.assertEqual(miss_en, frozenset({"foo"}))
+        self.assertEqual(miss_dis, frozenset())
+        self.assertEqual(req, frozenset())  # has default, not required
+
+    def test_missing_disabled_default(self):
+        _, _, miss_en, miss_dis, _, req = self._classify(("foo(-)",))
+        self.assertEqual(miss_en, frozenset())
+        self.assertEqual(miss_dis, frozenset({"foo"}))
+        self.assertEqual(req, frozenset())
+
+    def test_disabled_with_default(self):
+        _, dis, miss_en, _, _, req = self._classify(("-foo(+)",))
+        self.assertEqual(dis, frozenset({"foo"}))
+        self.assertEqual(miss_en, frozenset({"foo"}))
+        self.assertEqual(req, frozenset())
+
+    def test_mixed(self):
+        en, dis, _, _, cond, req = self._classify(("foo", "-bar", "!baz?", "qux="))
+        self.assertEqual(en, frozenset({"foo"}))
+        self.assertEqual(dis, frozenset({"bar"}))
+        self.assertIsNotNone(cond)
+        self.assertEqual(cond["disabled"], frozenset({"baz"}))
+        self.assertEqual(cond["equal"], frozenset({"qux"}))
+        self.assertEqual(req, frozenset({"foo", "bar", "baz", "qux"}))
+
+    def test_flag_with_hyphen(self):
+        en, dis, _, _, cond, req = self._classify(("foo-bar",))
+        self.assertEqual(en, frozenset({"foo-bar"}))
+        self.assertEqual(req, frozenset({"foo-bar"}))
+
+    def test_disabled_flag_with_hyphen(self):
+        _, dis, _, _, _, req = self._classify(("-foo-bar",))
+        self.assertEqual(dis, frozenset({"foo-bar"}))
+        self.assertEqual(req, frozenset({"foo-bar"}))
+
+    def test_conditional_flag_with_hyphen(self):
+        _, _, _, _, cond, _ = self._classify(("foo-bar?",))
+        self.assertEqual(cond["enabled"], frozenset({"foo-bar"}))
+
+    def test_flag_with_plus(self):
+        en, _, _, _, _, _ = self._classify(("c++",))
+        self.assertEqual(en, frozenset({"c++"}))
+
+    def test_conditional_flag_with_plus(self):
+        _, _, _, _, cond, _ = self._classify(("c++?",))
+        self.assertEqual(cond["enabled"], frozenset({"c++"}))
+
+    def test_flag_with_at(self):
+        en, _, _, _, _, _ = self._classify(("LINGUAS_en@euro",))
+        self.assertEqual(en, frozenset({"LINGUAS_en@euro"}))
+
+    def test_disabled_flag_with_at(self):
+        _, dis, _, _, _, _ = self._classify(("-LINGUAS_en@euro",))
+        self.assertEqual(dis, frozenset({"LINGUAS_en@euro"}))
+
+    def test_invalid_token_raises(self):
+        for bad in ("!foo", "!!foo", "?", "=", "foo??", ""):
+            with self.subTest(token=bad):
+                with self.assertRaises(ValueError):
+                    self._parser.classify_use_deps((bad,))
+
+    def test_matches_python_use_dep(self):
+        eapi_attrs = _get_eapi_attrs("8")
+        cases = [
+            ("foo",),
+            ("-foo",),
+            ("foo?",),
+            ("!foo?",),
+            ("foo=",),
+            ("!foo=",),
+            ("foo(+)",),
+            ("foo(-)",),
+            ("-foo(+)",),
+            ("foo", "-bar", "!baz?", "qux=", "quux(+)"),
+            ("a", "b?", "!c?", "d=", "!e=", "-f", "g(+)", "h(-)"),
+            ("foo-bar",),
+            ("-foo-bar",),
+            ("foo-bar?",),
+            ("c++",),
+            ("-c++",),
+            ("c++?",),
+            ("LINGUAS_en@euro",),
+            ("-LINGUAS_en@euro",),
+        ]
+        for tokens in cases:
+            with self.subTest(tokens=tokens):
+                en, dis, miss_en, miss_dis, cond, req = self._parser.classify_use_deps(
+                    tokens
+                )
+                c_use = _use_dep(
+                    tokens,
+                    eapi_attrs,
+                    enabled_flags=en,
+                    disabled_flags=dis,
+                    missing_enabled=miss_en,
+                    missing_disabled=miss_dis,
+                    conditional=cond,
+                    required=req,
+                )
+                py_use = _use_dep(list(tokens), eapi_attrs)
+                for attr in (
+                    "enabled",
+                    "disabled",
+                    "required",
+                    "missing_enabled",
+                    "missing_disabled",
+                ):
+                    self.assertEqual(
+                        getattr(c_use, attr),
+                        getattr(py_use, attr),
+                        f"{attr} mismatch for {tokens}",
+                    )
+                if py_use.conditional is None:
+                    self.assertIsNone(c_use.conditional)
+                else:
+                    self.assertIsNotNone(c_use.conditional)
+                    for k in ("enabled", "disabled", "equal", "not_equal"):
+                        self.assertEqual(
+                            getattr(c_use.conditional, k, frozenset()),
+                            getattr(py_use.conditional, k, frozenset()),
+                            f"conditional.{k} for {tokens}",
+                        )
+
+
+class TestCFastVsPython(TestCase):
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _compare(self, depstr, uselist=None, matchall=False, eapi="8"):
+        kw = dict(token_class=Atom, matchall=matchall, eapi=eapi, uselist=uselist or [])
+        with _use_c_parser(False):
+            py_result = use_reduce(depstr, **kw)
+        c_result = use_reduce(depstr, **kw)
+        ok, msg = _result_equal(c_result, py_result)
+        self.assertTrue(ok, f"{depstr!r}: {msg}")
+
+    def test_simple_atom(self):
+        self._compare("dev-libs/foo")
+
+    def test_versioned_atom(self):
+        self._compare("=dev-libs/foo-1.2.3-r1")
+
+    def test_blocker_weak(self):
+        self._compare("!dev-libs/foo")
+
+    def test_blocker_strong(self):
+        self._compare("!!dev-libs/foo")
+
+    def test_slot(self):
+        self._compare("dev-libs/foo:1")
+
+    def test_sub_slot(self):
+        self._compare("dev-libs/foo:1/2")
+
+    def test_slot_operator(self):
+        self._compare("dev-libs/foo:=")
+
+    def test_slot_and_operator(self):
+        self._compare("dev-libs/foo:1=")
+
+    def test_use_enabled(self):
+        self._compare("dev-libs/foo[bar]", matchall=True)
+
+    def test_use_disabled(self):
+        self._compare("dev-libs/foo[-bar]", matchall=True)
+
+    def test_use_multiple(self):
+        self._compare("dev-libs/foo[a,b,c,-d]", matchall=True)
+
+    def test_use_conditional_enabled(self):
+        self._compare("dev-libs/foo[bar?]", matchall=True)
+
+    def test_use_conditional_disabled(self):
+        self._compare("dev-libs/foo[!bar?]", matchall=True)
+
+    def test_use_equal(self):
+        self._compare("dev-libs/foo[bar=]", matchall=True)
+
+    def test_use_not_equal(self):
+        self._compare("dev-libs/foo[!bar=]", matchall=True)
+
+    def test_use_miss_en_default(self):
+        self._compare("dev-libs/foo[bar(+)]", matchall=True)
+
+    def test_use_miss_dis_default(self):
+        self._compare("dev-libs/foo[bar(-)]", matchall=True)
+
+    def test_use_complex(self):
+        self._compare("=sys-apps/portage-2.1-r1:0[doc,a=,!b=,c?,!d?,-e]", matchall=True)
+
+    def test_cond_eval_active(self):
+        self._compare("dev-libs/foo[bar?]", uselist=["bar"])
+
+    def test_cond_eval_inactive(self):
+        self._compare("dev-libs/foo[bar?]", uselist=[])
+
+    def test_cond_not_eq_active(self):
+        self._compare("dev-libs/foo[!bar=]", uselist=["bar"])
+
+    def test_or_group(self):
+        self._compare("|| ( dev-libs/a dev-libs/b )", matchall=True)
+
+    def test_use_cond_group(self):
+        self._compare("foo? ( dev-libs/a dev-libs/b )", uselist=["foo"])
+
+    def test_nested_groups(self):
+        self._compare(
+            "a? ( || ( dev-libs/a1 dev-libs/a2 ) b? ( dev-libs/b ) )",
+            uselist=["a", "b"],
+        )
+
+    def test_complex(self):
+        self._compare(
+            "dev-libs/A >=dev-libs/B-2.0 || ( dev-libs/C dev-libs/D ) "
+            "foo? ( =dev-libs/E-1.0:0[bar,baz?] )",
+            uselist=["foo"],
+        )
+
+    def test_matchall(self):
+        self._compare("a? ( dev-libs/A ) !b? ( dev-libs/B )", matchall=True)
+
+    def test_eapi5(self):
+        self._compare("dev-libs/foo[bar=]", matchall=True, eapi="5")
+
+    def test_eapi6(self):
+        self._compare("dev-libs/foo:0/1=[bar,!baz?]", matchall=True, eapi="6")
+
+    # '@' in USE flag names, deprecated but allowed per PMS (old LINGUAS flags)
+    def test_use_at_sign(self):
+        self._compare("dev-libs/foo[LINGUAS_en@euro]", matchall=True)
+
+    def test_use_at_sign_disabled(self):
+        self._compare("dev-libs/foo[-LINGUAS_en@euro]", matchall=True)
+
+    def test_glob_version(self):
+        self._compare("=dev-libs/foo-1.2*", matchall=True)
+
+    def test_slot_operator_bare_eq(self):
+        self._compare("dev-libs/foo:=", matchall=True)
+
+    def test_invalid_raises_same_exception(self):
+        bad_cases = [
+            "|| ( dev-libs/a dev-libs/b",
+            "dev-libs/a )",
+            ">=dev-libs/foo",
+        ]
+        kw = dict(token_class=Atom, matchall=True, eapi="8", uselist=[])
+        for depstr in bad_cases:
+            with self.subTest(depstr=depstr):
+                with _use_c_parser(False):
+                    with self.assertRaises(InvalidDependString):
+                        use_reduce(depstr, **kw)
+                with self.assertRaises(InvalidDependString):
+                    use_reduce(depstr, **kw)
+
+
+class TestDeepNesting(TestCase):
+    """The C parser keeps its group stack on the heap, so it must not impose a
+    nesting limit the pure-Python path does not have."""
+
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _reduce(self, depstr, use_c):
+        with _use_c_parser(use_c):
+            return use_reduce(
+                depstr, token_class=Atom, matchall=True, eapi="8", uselist=[]
+            )
+
+    def test_nesting_parity(self):
+        for depth in (1, 8, 31, 32, 33, 64, 65, 200):
+            with self.subTest(depth=depth):
+                depstr = "( " * depth + "dev-libs/a" + " )" * depth
+                self.assertEqual(
+                    self._reduce(depstr, use_c=True),
+                    self._reduce(depstr, use_c=False),
+                )
+
+
+class TestLongAtoms(TestCase):
+    """Category and package names are not length-limited, so the C path must
+    not reject an atom that the pure-Python path accepts."""
+
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _reduce(self, depstr, use_c):
+        with _use_c_parser(use_c):
+            return use_reduce(
+                depstr, token_class=Atom, matchall=True, eapi="8", uselist=[]
+            )
+
+    def test_long_package_name(self):
+        for length in (8, 250, 255, 256, 300, 512, 1000):
+            with self.subTest(length=length):
+                depstr = "cat/" + "a" * length
+                self.assertEqual(
+                    self._reduce(depstr, use_c=True),
+                    self._reduce(depstr, use_c=False),
+                )
+
+    def test_long_versioned_atom(self):
+        for length in (250, 256, 600):
+            with self.subTest(length=length):
+                depstr = "=cat/" + "a" * length + "-1.0"
+                c = self._reduce(depstr, use_c=True)
+                py = self._reduce(depstr, use_c=False)
+                self.assertEqual(c, py)
+                self.assertEqual(c[0]._cpv, py[0]._cpv)
+
+    def test_long_category(self):
+        depstr = "c" * 400 + "/pkg"
+        self.assertEqual(
+            self._reduce(depstr, use_c=True), self._reduce(depstr, use_c=False)
+        )
+
+
+class TestCParserMalformedGroups(TestCase):
+    """Whitespace and delimiter errors in group syntax. Every one of these has
+    a distinct error path in scan_item()/scan_group_contents(), and each must
+    be rejected by both parsers."""
+
+    CASES = (
+        "( dev-libs/a",  # no closing paren
+        "( dev-libs/a dev-libs/b",  # no closing paren, multiple items
+        "|| ( dev-libs/a ",  # trailing whitespace, still unterminated
+        "(dev-libs/a )",  # no whitespace after '('
+        "|| (dev-libs/a )",  # no whitespace after '(' in an any-of
+        "foo? (dev-libs/a )",  # no whitespace after '(' in a conditional
+        "||( dev-libs/a )",  # no whitespace after the '||' prefix
+        "foo?( dev-libs/a )",  # no whitespace after the 'foo?' prefix
+        "|| dev-libs/a",  # any-of with no group at all
+        "foo? dev-libs/a",  # conditional with no group at all
+        "|| dev-libs/a dev-libs/b",
+        "( dev-libs/a )dev-libs/b",  # no whitespace after ')'
+        "|| (",
+        "foo?",
+        "||",
+    )
+
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _reduce(self, depstr, use_c):
+        with _use_c_parser(use_c):
+            return use_reduce(depstr, token_class=Atom, eapi="8", uselist=["foo"])
+
+    def test_both_paths_reject(self):
+        for depstr in self.CASES:
+            with self.subTest(depstr=depstr):
+                with self.assertRaises(InvalidDependString):
+                    self._reduce(depstr, use_c=False)
+                with self.assertRaises(InvalidDependString):
+                    self._reduce(depstr, use_c=True)
+
+
+class TestCParserWhitespace(TestCase):
+    """The C parser sees the dep string unsplit, so leading, trailing and
+    repeated whitespace is its own business rather than str.split()'s."""
+
+    CASES = (
+        "dev-libs/a ",
+        " dev-libs/a",
+        "\tdev-libs/a\n",
+        "dev-libs/a  dev-libs/b",
+        "|| ( dev-libs/a dev-libs/b )   ",
+        "\n|| (\n\tdev-libs/a\n\tdev-libs/b\n)\n",
+        "   ",
+        "",
+    )
+
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _reduce(self, depstr, use_c):
+        with _use_c_parser(use_c):
+            return use_reduce(depstr, token_class=Atom, eapi="8", matchall=True)
+
+    def test_parity(self):
+        for depstr in self.CASES:
+            with self.subTest(depstr=depstr):
+                py = self._reduce(depstr, use_c=False)
+                c = self._reduce(depstr, use_c=True)
+                ok, msg = _result_equal(c, py)
+                self.assertTrue(ok, f"{depstr!r}: {msg}")
+
+
+class TestCParserInactiveConditionalBodies(TestCase):
+    """The body of an inactive use conditional is not emitted, but it is still
+    parsed with the real grammar (via the skip visitor) so that syntax errors
+    inside it are still reported. Nested groups and nested conditionals inside
+    an inactive body are the interesting cases."""
+
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _reduce(self, depstr, use_c, uselist=()):
+        with _use_c_parser(use_c):
+            return use_reduce(depstr, token_class=Atom, eapi="8", uselist=list(uselist))
+
+    VALID = (
+        ("foo? ( ( dev-libs/a ) )", ()),
+        ("foo? ( || ( dev-libs/a dev-libs/b ) )", ()),
+        ("foo? ( bar? ( dev-libs/a ) )", ()),
+        ("foo? ( bar? ( dev-libs/a ) )", ("bar",)),
+        ("!foo? ( bar? ( dev-libs/a ) )", ("foo", "bar")),
+        ("foo? ( !bar? ( dev-libs/a ) )", ()),
+        ("foo? ( bar? ( || ( dev-libs/a dev-libs/b ) ) )", ()),
+        ("foo? ( dev-libs/a ) bar? ( dev-libs/b )", ("bar",)),
+    )
+
+    # Syntax errors that only appear inside a body that is never emitted.
+    INVALID = (
+        ("foo? ( ||( dev-libs/a ) )", ()),
+        ("foo? ( (dev-libs/a ) )", ()),
+        ("foo? ( bar? ( noslash ) )", ()),
+        ("foo? ( bar? ( dev-libs/a )", ()),
+        ("foo? ( >=dev-libs/a )", ()),
+    )
+
+    def test_inactive_bodies_parity(self):
+        for depstr, uselist in self.VALID:
+            with self.subTest(depstr=depstr, uselist=uselist):
+                py = self._reduce(depstr, use_c=False, uselist=uselist)
+                c = self._reduce(depstr, use_c=True, uselist=uselist)
+                ok, msg = _result_equal(c, py)
+                self.assertTrue(ok, f"{depstr!r} uselist={uselist}: {msg}")
+
+    def test_errors_inside_inactive_bodies_still_raise(self):
+        for depstr, uselist in self.INVALID:
+            with self.subTest(depstr=depstr, uselist=uselist):
+                with self.assertRaises(InvalidDependString):
+                    self._reduce(depstr, use_c=False, uselist=uselist)
+                with self.assertRaises(InvalidDependString):
+                    self._reduce(depstr, use_c=True, uselist=uselist)
+
+
+class TestCAtomObjectProtocol(TestCase):
+    """_parser.Atom's tp_repr/tp_str/tp_hash/tp_richcompare slots."""
+
+    def setUp(self):
+        self._parser = _c_parser()
+        if self._parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _atom(self, s):
+        return self._parser.parse(s, matchall=True)[0]
+
+    def test_str(self):
+        self.assertEqual(
+            str(self._atom(">=dev-libs/a-1:2/3=[x]")), ">=dev-libs/a-1:2/3=[x]"
+        )
+
+    def test_repr(self):
+        self.assertEqual(repr(self._atom("dev-libs/a")), "Atom('dev-libs/a')")
+
+    def test_hash_matches_string(self):
+        self.assertEqual(hash(self._atom("dev-libs/a")), hash("dev-libs/a"))
+
+    def test_hashable_in_containers(self):
+        a, b = self._atom("dev-libs/a"), self._atom("dev-libs/a")
+        self.assertEqual(len({a, b}), 1)
+        self.assertEqual({a: 1}[b], 1)
+
+    def test_equality(self):
+        a, b = self._atom("dev-libs/a"), self._atom("dev-libs/a")
+        c = self._atom("dev-libs/b")
+        self.assertTrue(a == b)
+        self.assertFalse(a != b)
+        self.assertFalse(a == c)
+        self.assertTrue(a != c)
+
+    def test_equality_with_foreign_type(self):
+        a = self._atom("dev-libs/a")
+        for other in ("dev-libs/a", 1, None, Atom("dev-libs/a")):
+            with self.subTest(other=other):
+                self.assertFalse(a == other)
+                self.assertTrue(a != other)
+
+    def test_ordering_is_not_implemented(self):
+        a, b = self._atom("dev-libs/a"), self._atom("dev-libs/b")
+        for op in (operator.lt, operator.le, operator.gt, operator.ge):
+            with self.subTest(op=op.__name__):
+                with self.assertRaises(TypeError):
+                    op(a, b)
+
+
+class TestCParserEapiGuard(TestCase):
+    """The C scanner implements the modern (EAPI 5+) atom grammar
+    unconditionally. The use_reduce fast-path guard must therefore only take
+    the C path for EAPIs whose grammar matches -- None (permissive) or
+    slot_operator-capable (EAPI 5+) -- so it never accepts atoms that the
+    pure-Python path rejects under an older EAPI."""
+
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _reduce(self, depstr, eapi, use_c):
+        with _use_c_parser(use_c):
+            return use_reduce(
+                depstr, token_class=Atom, matchall=True, eapi=eapi, uselist=[]
+            )
+
+    def test_slot_operator_gate_matches_expectation(self):
+        for eapi in ("0", "1", "4"):
+            self.assertFalse(_get_eapi_attrs(eapi).slot_operator, eapi)
+        for eapi in (None, "5", "6", "7", "8"):
+            self.assertTrue(_get_eapi_attrs(eapi).slot_operator, eapi)
+
+    def test_old_eapi_rejects_modern_syntax(self):
+        # Each string is invalid under the given older EAPI. The Python path
+        # rejects it; the C path is guard-skipped for these EAPIs, so it must
+        # reject identically rather than silently accept.
+        cases = [
+            ("dev-libs/foo:=", "4"),  # slot operator: EAPI 5+
+            ("dev-libs/foo:1/2", "4"),  # sub-slot: EAPI 5+
+            ("dev-libs/foo[bar]", "1"),  # use dep: EAPI 4+
+            ("dev-libs/foo:1", "0"),  # slot dep: EAPI 1+
+        ]
+        for depstr, eapi in cases:
+            with self.subTest(depstr=depstr, eapi=eapi):
+                with self.assertRaises(InvalidDependString):
+                    self._reduce(depstr, eapi, use_c=False)
+                with self.assertRaises(InvalidDependString):
+                    self._reduce(depstr, eapi, use_c=True)
+
+    def test_valid_across_eapis_parity(self):
+        cases = [
+            ("dev-libs/foo", None),
+            ("dev-libs/foo", "0"),
+            ("dev-libs/foo", "8"),
+            ("dev-libs/foo:1", "1"),
+            ("dev-libs/foo:1", "8"),
+            ("dev-libs/foo[bar]", "4"),
+            ("dev-libs/foo[bar]", "8"),
+            ("dev-libs/foo:=", "5"),
+            ("dev-libs/foo:1/2=", "8"),
+        ]
+        for depstr, eapi in cases:
+            with self.subTest(depstr=depstr, eapi=eapi):
+                py = self._reduce(depstr, eapi, use_c=False)
+                c = self._reduce(depstr, eapi, use_c=True)
+                ok, msg = _result_equal(c, py)
+                self.assertTrue(ok, f"{depstr!r} eapi={eapi}: {msg}")
+
+
+class TestKillSwitch(TestCase):
+    """PORTAGE_NATIVE_DEP_PARSER=0 must disable the C path.  It is consulted
+    when portage.dep is imported, so this has to run in a fresh process."""
+
+    PROBE = "import portage.dep; print(portage.dep._c_dep_parser is None)"
+
+    def setUp(self):
+        if _orig_c_dep_parser is None:
+            self.skipTest("_parser extension not available")
+
+    def _probe(self, value):
+        env = dict(os.environ)
+        env["PYTHONPATH"] = os.pathsep.join(
+            [str(Path(portage.__file__).parent.parent), env.get("PYTHONPATH", "")]
+        )
+        if value is None:
+            env.pop("PORTAGE_NATIVE_DEP_PARSER", None)
+        else:
+            env["PORTAGE_NATIVE_DEP_PARSER"] = value
+        out = subprocess.run(
+            [sys.executable, "-c", self.PROBE],
+            env=env,
+            capture_output=True,
+            text=True,
+            check=True,
+        )
+        return out.stdout.strip()
+
+    def test_unset_uses_c_parser(self):
+        self.assertEqual(self._probe(None), "False")
+
+    def test_zero_disables_c_parser(self):
+        self.assertEqual(self._probe("0"), "True")
+
+    def test_other_values_do_not_disable(self):
+        # Only the exact string "0" disables it.
+        for value in ("1", "", "no"):
+            with self.subTest(value=value):
+                self.assertEqual(self._probe(value), "False")
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.