From af1640444e0fa53b67611984524b201936d71b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Thu, 14 Nov 2024 10:21:50 +0100 Subject: [PATCH 1/2] Improve error message for missing component requirements --- conan/tools/apple/xcodedeps.py | 2 +- .../templates/target_configuration.py | 2 +- conans/model/build_info.py | 28 ++++--- test/integration/test_components_error.py | 76 +++++++++++++++++++ 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/conan/tools/apple/xcodedeps.py b/conan/tools/apple/xcodedeps.py index 369e025ef63..a516cc7350c 100644 --- a/conan/tools/apple/xcodedeps.py +++ b/conan/tools/apple/xcodedeps.py @@ -313,7 +313,7 @@ def _transitive_components(component): else: public_deps.append((_format_name(d.ref.name),) * 2) - required_components = dep.cpp_info.required_components if dep.cpp_info.required_components else public_deps + required_components = [(c[0], c[1]) for c in dep.cpp_info.required_components] if dep.cpp_info.required_components else public_deps # In case dep is editable and package_folder=None pkg_folder = dep.package_folder or dep.recipe_folder root_content = self.get_content_for_component(require, dep_name, dep_name, pkg_folder, [dep.cpp_info], diff --git a/conan/tools/cmake/cmakedeps/templates/target_configuration.py b/conan/tools/cmake/cmakedeps/templates/target_configuration.py index 747b2bf011f..a6251f3362e 100644 --- a/conan/tools/cmake/cmakedeps/templates/target_configuration.py +++ b/conan/tools/cmake/cmakedeps/templates/target_configuration.py @@ -239,7 +239,7 @@ def get_deps_targets_names(self): # Declared cppinfo.requires or .components[].requires transitive_reqs = self.cmakedeps.get_transitive_requires(self.conanfile) if self.conanfile.cpp_info.required_components: - for dep_name, component_name in self.conanfile.cpp_info.required_components: + for dep_name, component_name, _ in self.conanfile.cpp_info.required_components: try: # if not dep_name, it is internal, from current self.conanfile req = transitive_reqs[dep_name] if dep_name is not None else self.conanfile diff --git a/conans/model/build_info.py b/conans/model/build_info.py index ca4d00e36ae..75e52c75507 100644 --- a/conans/model/build_info.py +++ b/conans/model/build_info.py @@ -685,10 +685,13 @@ def check_component_requires(self, conanfile): return # Accumulate all external requires comps = self.required_components - missing_internal = [c[1] for c in comps if c[0] is None and c[1] not in self.components] + missing_internal = [(c[1], c[2]) for c in comps if c[0] is None and c[1] not in self.components] if missing_internal: - raise ConanException(f"{conanfile}: Internal components not found: {missing_internal}") - external = [c[0] for c in comps if c[0] is not None] + msgs = [f"'{req}'" + + (f" (required from component '{req_comp}')" if req_comp else "") + for req, req_comp in missing_internal] + raise ConanException(f"{conanfile}: Internal components not found: {', '.join(msgs)}") + external = [(c[0], c[2]) for c in comps if c[0] is not None] if not external: return # Only direct host (not test) dependencies can define required components @@ -697,11 +700,13 @@ def check_component_requires(self, conanfile): direct_dependencies = [r.ref.name for r, d in conanfile.dependencies.items() if r.direct and not r.build and not r.is_test and r.visible and not r.override] - for e in external: + for e, req_comp in external: if e not in direct_dependencies: + extra_msg = f" (required from component '{req_comp}')" if req_comp else "" raise ConanException( - f"{conanfile}: required component package '{e}::' not in dependencies") + f"{conanfile}: required component package '{e}::'{extra_msg} not in dependencies") # TODO: discuss if there are cases that something is required but not transitive + external = [e[0] for e in external] for e in direct_dependencies: if e not in external: raise ConanException( @@ -709,17 +714,18 @@ def check_component_requires(self, conanfile): @property def required_components(self): - """Returns a list of tuples with (require, component_name) required by the package - If the require is internal (to another component), the require will be None""" + """Returns a list of tuples with (require, component_name, dependee_component_name) required by the package + - If the require is internal (to another component), the require will be None + - If the requirement comes from the main cpp_info, the dependee_component_name will be None""" # FIXME: Cache the value # First aggregate without repetition, respecting the order - ret = [r for r in self._package.requires] - for comp in self.components.values(): + ret = [(r, None) for r in self._package.requires] + for comp_name, comp in self.components.items(): for r in comp.requires: if r not in ret: - ret.append(r) + ret.append((r, comp_name)) # Then split the names - ret = [r.split("::") if "::" in r else (None, r) for r in ret] + ret = [(*r[0].split("::"), r[1]) if "::" in r[0] else (None, *r) for r in ret] return ret def deduce_full_cpp_info(self, conanfile): diff --git a/test/integration/test_components_error.py b/test/integration/test_components_error.py index fd78c4ea29b..71006a7c114 100644 --- a/test/integration/test_components_error.py +++ b/test/integration/test_components_error.py @@ -1,6 +1,9 @@ import os import textwrap +import pytest + +from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @@ -68,3 +71,76 @@ def package_info(self): c.save({"conanfile.py": conanfile}) c.run("create .", assert_error=True) assert 'The expected type for test_property is "list", but "str" was found' in c.out + + +@pytest.mark.parametrize("component", [True, False]) +def test_unused_requirement(component): + """ Requires should include all listed requirements + This error is known when creating the package if the requirement is consumed. + """ + + t = TestClient(light=True) + conanfile = textwrap.dedent(f""" + from conan import ConanFile + class Consumer(ConanFile): + name = "wrong" + version = "version" + requires = "top/version", "top2/version" + + def package_info(self): + self.cpp_info{'.components["foo"]' if component else ''}.requires = ["top::other"] + """) + t.save({"top/conanfile.py": GenConanfile().with_package_info({"components": {"cmp1": {"libs": ["top_cmp1"]}}}), + "conanfile.py": conanfile}) + t.run('create top --name=top --version=version') + t.run('create top --name=top2 --version=version') + t.run('create .', assert_error=True) + assert "ERROR: wrong/version: Required package 'top2' not in component 'requires" in t.out + + +@pytest.mark.parametrize("component", [True, False]) +def test_wrong_requirement(component): + """ If we require a wrong requirement, we get a meaninful error. + This error is known when creating the package if the requirement is not there. + """ + t = TestClient(light=True) + conanfile = textwrap.dedent(f""" + from conan import ConanFile + class Consumer(ConanFile): + name = "wrong" + version = "version" + requires = "top/version" + + def package_info(self): + self.cpp_info{'.components["foo"]' if component else ''}.requires = ["top::cmp1", "other::other"] + """) + t.save({"top/conanfile.py": GenConanfile().with_package_info({"components": {"cmp1": {"libs": ["top_cmp1"]}}}), + "conanfile.py": conanfile}) + t.run('create top --name=top --version=version') + t.run('create .', assert_error=True) + if component: + assert "ERROR: wrong/version: required component package 'other::' (required from component 'foo') not in dependencies" in t.out + else: + assert "ERROR: wrong/version: required component package 'other::' not in dependencies" in t.out + + +@pytest.mark.parametrize("component", [True, False]) +def test_missing_internal(component): + consumer = textwrap.dedent(f""" + from conan import ConanFile + + class Recipe(ConanFile): + def package_info(self): + self.cpp_info{'.components["foo"]' if component else ''}.requires = ["other", "another"] + self.cpp_info{'.components["bar"]' if component else ''}.requires = ["other", "another"] + """) + t = TestClient(light=True) + t.save({'conanfile.py': consumer}) + t.run('create . --name=wrong --version=version', assert_error=True) + if component: + assert ("ERROR: wrong/version: Internal components not found: 'other' (required from component 'foo')," + " 'another' (required from component 'foo')," + " 'other' (required from component 'bar')," + " 'another' (required from component 'bar')") in t.out + else: + assert "ERROR: wrong/version: Internal components not found: 'other', 'another'" in t.out From e969bef760a438402a3fdfef91dcfe342bf29cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Thu, 14 Nov 2024 10:28:59 +0100 Subject: [PATCH 2/2] =?UTF-8?q?Remove=20duplicated=20tests=C2=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmakedeps/test_cmakedeps_components.py | 79 ------------------- 1 file changed, 79 deletions(-) diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components.py index d3cead0b9ea..c63a4a0f721 100644 --- a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components.py +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components.py @@ -116,85 +116,6 @@ def package_info(self): assert "Component 'top::not-existing' not found in 'top' package requirement" in t.out -# TODO: This is CMakeDeps Independent, move it out of here -def test_unused_requirement(top_conanfile): - """ Requires should include all listed requirements - This error is known when creating the package if the requirement is consumed. - """ - consumer = textwrap.dedent(""" - from conan import ConanFile - - class Recipe(ConanFile): - requires = "top/version", "top2/version" - def package_info(self): - self.cpp_info.requires = ["top::other"] - """) - t = TestClient() - t.save({'top.py': top_conanfile, 'consumer.py': consumer}) - t.run('create top.py --name=top --version=version') - t.run('create top.py --name=top2 --version=version') - t.run('create consumer.py --name=wrong --version=version', assert_error=True) - assert "ERROR: wrong/version: Required package 'top2' not in component 'requires" in t.out - - - -# TODO: This is CMakeDeps Independent, move it out of here -def test_unused_tool_requirement(top_conanfile): - """ Requires should include all listed requirements - This error is known when creating the package if the requirement is consumed. - """ - consumer = textwrap.dedent(""" - from conan import ConanFile - - class Recipe(ConanFile): - requires = "top/version" - tool_requires = "top2/version" - def package_info(self): - self.cpp_info.requires = ["top::other"] - """) - t = TestClient() - t.save({'top.py': top_conanfile, 'consumer.py': consumer}) - t.run('create top.py --name=top --version=version') - t.run('create top.py --name=top2 --version=version') - t.run('create consumer.py --name=wrong --version=version') - # This runs without crashing, because it is not chcking that top::other doesn't exist - - -# TODO: This is CMakeDeps Independent, move it out of here -def test_wrong_requirement(top_conanfile): - """ If we require a wrong requirement, we get a meaninful error. - This error is known when creating the package if the requirement is not there. - """ - consumer = textwrap.dedent(""" - from conan import ConanFile - - class Recipe(ConanFile): - requires = "top/version" - def package_info(self): - self.cpp_info.requires = ["top::cmp1", "other::other"] - """) - t = TestClient() - t.save({'top.py': top_conanfile, 'consumer.py': consumer}) - t.run('create top.py --name=top --version=version') - t.run('create consumer.py --name=wrong --version=version', assert_error=True) - assert "ERROR: wrong/version: required component package 'other::' not in dependencies" in t.out - - -# TODO: This is CMakeDeps Independent, move it out of here -def test_missing_internal(): - consumer = textwrap.dedent(""" - from conan import ConanFile - - class Recipe(ConanFile): - def package_info(self): - self.cpp_info.components["cmp1"].requires = ["other"] - """) - t = TestClient() - t.save({'conanfile.py': consumer}) - t.run('create . --name=wrong --version=version', assert_error=True) - assert "ERROR: wrong/version: Internal components not found: ['other']" in t.out - - @pytest.mark.tool("cmake") def test_components_system_libs(): conanfile = textwrap.dedent("""