When USE=test is not enabled, allow circular test dependencies
by treating them like PDEPEND. When USE=test is enabled, circular
dependencies are still not allowed, as shown in unit tests.

Suggested-by: Michał Górny <mgo...@gentoo.org>
Bug: https://bugs.gentoo.org/703348
Signed-off-by: Zac Medico <zmed...@gentoo.org>
---
 lib/_emerge/depgraph.py                       | 18 +++++++--
 lib/portage/dep/__init__.py                   | 32 ++++++++++++++-
 lib/portage/tests/dep/test_use_reduce.py      | 30 +++++++++++++-
 .../tests/resolver/test_with_test_deps.py     | 39 ++++++++++++++++++-
 4 files changed, 111 insertions(+), 8 deletions(-)

diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py
index 1a5448c8f..610a0fc47 100644
--- a/lib/_emerge/depgraph.py
+++ b/lib/_emerge/depgraph.py
@@ -3325,10 +3325,6 @@ class depgraph(object):
                        pkg.iuse.is_valid_flag("test") and \
                        self._is_argument(pkg)
 
-               if with_test_deps:
-                       use_enabled = set(use_enabled)
-                       use_enabled.add("test")
-
                if not pkg.built and \
                        "--buildpkgonly" in self._frozen_config.myopts and \
                        "deep" not in self._dynamic_config.myparams:
@@ -3430,6 +3426,20 @@ class depgraph(object):
                                                noiselevel=-1, 
level=logging.DEBUG)
 
                                try:
+                                       if with_test_deps and 'test' not in 
use_enabled:
+                                               test_deps = 
portage.dep.use_reduce(dep_string,
+                                                       uselist=use_enabled | 
{'test'},
+                                                       
is_valid_flag=pkg.iuse.is_valid_flag,
+                                                       opconvert=True, 
token_class=Atom,
+                                                       eapi=pkg.eapi,
+                                                       subset={'test'})
+
+                                               if test_deps and not 
self._add_pkg_dep_string(
+                                                       pkg, dep_root, 
self._priority(runtime_post=True),
+                                                       test_deps,
+                                                       allow_unsatisfied):
+                                                       return 0
+
                                        dep_string = 
portage.dep.use_reduce(dep_string,
                                                uselist=use_enabled,
                                                
is_valid_flag=pkg.iuse.is_valid_flag,
diff --git a/lib/portage/dep/__init__.py b/lib/portage/dep/__init__.py
index f08f6ba4c..eaa200751 100644
--- a/lib/portage/dep/__init__.py
+++ b/lib/portage/dep/__init__.py
@@ -405,7 +405,8 @@ def paren_enclose(mylist, unevaluated_atom=False, 
opconvert=False):
        return " ".join(mystrparts)
 
 def use_reduce(depstr, uselist=(), masklist=(), matchall=False, excludeall=(), 
is_src_uri=False, \
-       eapi=None, opconvert=False, flat=False, is_valid_flag=None, 
token_class=None, matchnone=False):
+       eapi=None, opconvert=False, flat=False, is_valid_flag=None, 
token_class=None, matchnone=False,
+       subset=None):
        """
        Takes a dep string and reduces the use? conditionals out, leaving an 
array
        with subarrays. All redundant brackets are removed.
@@ -434,6 +435,8 @@ def use_reduce(depstr, uselist=(), masklist=(), 
matchall=False, excludeall=(), i
        @type token_class: Class
        @param matchnone: Treat all conditionals as inactive. Used by 
digestgen(). 
        @type matchnone: Bool
+       @param subset: Select a subset of dependencies conditional on the given 
flags
+       @type subset: Sequence
        @rtype: List
        @return: The use reduced depend array
        """
@@ -491,6 +494,33 @@ def use_reduce(depstr, uselist=(), masklist=(), 
matchall=False, excludeall=(), i
                return (flag in uselist and not is_negated) or \
                        (flag not in uselist and is_negated)
 
+       if subset:
+               def select_subset(dep_struct, disjunction, selected):
+                       result = []
+                       stack = list(dep_struct)
+                       stack.reverse()
+                       while stack:
+                               token = stack.pop()
+                               try:
+                                       conditional = token.endswith('?')
+                               except AttributeError:
+                                       result.extend(token)
+                               else:
+                                       if conditional:
+                                               children = stack.pop()
+                                               if is_active(token) and 
token[:-1] in subset:
+                                                       if disjunction:
+                                                               
result.append(select_subset(children, True, True))
+                                                       else:
+                                                               
result.extend(select_subset(children, False, True))
+                                       elif token == '||':
+                                               result.append(token)
+                                               
result.append(select_subset(stack.pop(), True, selected))
+                                       elif selected:
+                                               result.append(token)
+                       return result
+               depstr = paren_enclose(select_subset(paren_reduce(depstr, 
_deprecation_warn=False), False, False))
+
        def missing_white_space_check(token, pos):
                """
                Used to generate good error messages for invalid tokens.
diff --git a/lib/portage/tests/dep/test_use_reduce.py 
b/lib/portage/tests/dep/test_use_reduce.py
index 4f65567cf..3435b6116 100644
--- a/lib/portage/tests/dep/test_use_reduce.py
+++ b/lib/portage/tests/dep/test_use_reduce.py
@@ -9,7 +9,7 @@ class UseReduceTestCase(object):
        def __init__(self, deparray, uselist=[], masklist=[],
                     matchall=0, excludeall=[], is_src_uri=False,
                     eapi='0', opconvert=False, flat=False, 
expected_result=None,
-                    is_valid_flag=None, token_class=None):
+                    is_valid_flag=None, token_class=None, subset=None):
                self.deparray = deparray
                self.uselist = uselist
                self.masklist = masklist
@@ -21,13 +21,15 @@ class UseReduceTestCase(object):
                self.flat = flat
                self.is_valid_flag = is_valid_flag
                self.token_class = token_class
+               self.subset = subset
                self.expected_result = expected_result
 
        def run(self):
                try:
                        return use_reduce(self.deparray, self.uselist, 
self.masklist,
                                self.matchall, self.excludeall, 
self.is_src_uri, self.eapi,
-                               self.opconvert, self.flat, self.is_valid_flag, 
self.token_class)
+                               self.opconvert, self.flat, self.is_valid_flag, 
self.token_class,
+                               subset=self.subset)
                except InvalidDependString as e:
                        raise InvalidDependString("%s: %s" % (e, self.deparray))
 
@@ -50,6 +52,30 @@ class UseReduce(TestCase):
                                uselist=["a", "b", "c", "d"],
                                expected_result=["A", "B"]
                                ),
+                       UseReduceTestCase(
+                               "a? ( A ) b? ( B ) !c? ( C ) !d? ( D )",
+                               uselist=["a", "b", "c", "d"],
+                               subset=["b"],
+                               expected_result=["B"]
+                               ),
+                       UseReduceTestCase(
+                               "bar? ( || ( foo bar? ( baz ) ) )",
+                               uselist=["bar"],
+                               subset=["bar"],
+                               expected_result=['||', ['foo', 'baz']]
+                               ),
+                       UseReduceTestCase(
+                               "bar? ( foo bar? ( baz ) foo )",
+                               uselist=["bar"],
+                               subset=["bar"],
+                               expected_result=['foo', 'baz', 'foo']
+                               ),
+                       UseReduceTestCase(
+                               "|| ( foo bar? ( baz ) )",
+                               uselist=["bar"],
+                               subset=["bar"],
+                               expected_result=["baz"]
+                               ),
                        UseReduceTestCase(
                                "a? ( A ) b? ( B ) !c? ( C ) !d? ( D )",
                                uselist=["a", "b", "c"],
diff --git a/lib/portage/tests/resolver/test_with_test_deps.py 
b/lib/portage/tests/resolver/test_with_test_deps.py
index 5bfc6a8a2..d88e3cb6e 100644
--- a/lib/portage/tests/resolver/test_with_test_deps.py
+++ b/lib/portage/tests/resolver/test_with_test_deps.py
@@ -21,7 +21,27 @@ class WithTestDepsTestCase(TestCase):
                        },
                        "app-misc/C-0": {
                                "EAPI": "5",
-                       }
+                       },
+                       "app-misc/D-0": {
+                               "EAPI": "5",
+                               "IUSE": "test",
+                               "DEPEND": "test? ( app-misc/E )"
+                       },
+                       "app-misc/E-0": {
+                               "EAPI": "5",
+                               "IUSE": "test",
+                               "DEPEND": "test? ( app-misc/D )"
+                       },
+                       "app-misc/F-0": {
+                               "EAPI": "5",
+                               "IUSE": "+test",
+                               "DEPEND": "test? ( app-misc/G )"
+                       },
+                       "app-misc/G-0": {
+                               "EAPI": "5",
+                               "IUSE": "+test",
+                               "DEPEND": "test? ( app-misc/F )"
+                       },
                }
 
                test_cases = (
@@ -32,6 +52,23 @@ class WithTestDepsTestCase(TestCase):
                                success = True,
                                options = { "--onlydeps": True, 
"--with-test-deps": True },
                                mergelist = ["app-misc/B-0"]),
+
+                       # Test that --with-test-deps allows circular 
dependencies.
+                       ResolverPlaygroundTestCase(
+                               ['app-misc/D'],
+                               success = True,
+                               options = {'--with-test-deps': True},
+                               mergelist = [('app-misc/D-0', 'app-misc/E-0')],
+                               ambiguous_merge_order=True),
+
+                       # Test that --with-test-deps does not allow circular 
dependencies
+                       # when USE=test is explicitly enabled.
+                       ResolverPlaygroundTestCase(
+                               ['app-misc/F'],
+                               success = False,
+                               options = {'--with-test-deps': True},
+                               circular_dependency_solutions = 
{'app-misc/G-0': {frozenset({('test', False)})}, 'app-misc/F-0': 
{frozenset({('test', False)})}},
+                       )
                )
 
                playground = ResolverPlayground(ebuilds=ebuilds, debug=False)
-- 
2.21.0


Reply via email to