Skip to content

Commit

Permalink
Add example test for forwards compat of custom setting (#17524)
Browse files Browse the repository at this point in the history
* Add example test for forwards compat of custom setting

* Cleanup

* Cppstd compat

* MSVC combination tested too

* refactored compatibility.py for easier extension

* add new checks to msvc 194->193

* fix test combinatorics

---------

Co-authored-by: memsharded <james@conan.io>
  • Loading branch information
AbrilRBS and memsharded authored Jan 21, 2025
1 parent ac0e86e commit 5a67746
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 28 deletions.
61 changes: 35 additions & 26 deletions conans/client/graph/compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,40 @@
def compatibility(conanfile):
configs = cppstd_compat(conanfile)
# TODO: Append more configurations for your custom compatibility rules
return configs
# By default, different compiler.cppstd are compatible
# factors is a list of lists
factors = cppstd_compat(conanfile)
# MSVC 194->193 fallback compatibility
compiler = conanfile.settings.get_safe("compiler")
compiler_version = conanfile.settings.get_safe("compiler.version")
if compiler == "msvc":
msvc_fallback = {"194": "193"}.get(compiler_version)
if msvc_fallback:
factors.append([{"compiler.version": msvc_fallback}])
# Append more factors for your custom compatibility rules here
# Combine factors to compute all possible configurations
combinations = _factors_combinations(factors)
# Final compatibility settings combinations to check
return [{"settings": [(k, v) for k, v in comb.items()]} for comb in combinations]
def _factors_combinations(factors):
combinations = []
for factor in factors:
if not combinations:
combinations = factor
continue
new_combinations = []
for comb in combinations:
for f in factor:
new_comb = comb.copy()
new_comb.update(f)
new_combinations.append(new_comb)
combinations.extend(new_combinations)
return combinations
"""


Expand Down Expand Up @@ -51,29 +82,7 @@ def cppstd_compat(conanfile):
conanfile.output.warning(f'No cstd compatibility defined for compiler "{compiler}"')
else:
factors.append([{"compiler.cstd": v} for v in cstd_possible_values if v != cstd])
if compiler == "msvc":
msvc_fallback = {"194": "193"}.get(compiler_version)
if msvc_fallback:
factors.append([{"compiler.version": msvc_fallback}])
combinations = []
for factor in factors:
if not combinations:
combinations = factor
continue
new_combinations = []
for comb in combinations:
for f in factor:
new_comb = comb.copy()
new_comb.update(f)
new_combinations.append(new_comb)
combinations.extend(new_combinations)
ret = []
for comb in combinations:
ret.append({"settings": [(k, v) for k, v in comb.items()]})
return ret
return factors
"""


Expand Down
64 changes: 62 additions & 2 deletions test/integration/package_id/compatible_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,13 +443,18 @@ def test_compatibility_msvc_and_cppstd(self):
compiler.runtime=dynamic
""")
tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0").with_setting("compiler"),
"conanfile.py": GenConanfile("app", "1.0").with_require("dep/1.0").with_setting("compiler"),
"profile": profile})

tc.run("create dep -pr=profile -s compiler.cppstd=20")
tc.run("create . -pr=profile -s compiler.cppstd=17")
tc.run("install --requires=dep/1.0 -pr=profile -s compiler.cppstd=17")
tc.assert_listed_binary({"dep/1.0": ("b6d26a6bc439b25b434113982791edf9cab4d004", "Cache")})

tc.run("remove * -c")
tc.run("create dep -pr=profile -s compiler.version=193 -s compiler.cppstd=20")
tc.run("install --requires=dep/1.0 -pr=profile -s compiler.cppstd=17")
assert "compiler.cppstd=20, compiler.version=193" in tc.out
tc.assert_listed_binary({"dep/1.0": ("535899bb58c3ca7d80a380313d31f4729e735d1c", "Cache")})


class TestCompatibleBuild:
def test_build_compatible(self):
Expand Down Expand Up @@ -706,3 +711,58 @@ def validate_build(self):
pkga = liba["packages"][0][pkg_index]
assert pkga["info"]["compatibility_delta"] == {"settings": [["compiler.cppstd", "14"]]}
assert pkga["build_args"] == "--requires=liba/0.1 --build=compatible:liba/0.1"


def test_compatibility_new_setting_forwards_compat():
""" This test tries to reflect the following scenario:
- User adds a new setting (libc.version in this case)
- This setting is forward compatible
How is it solved with compatibility.py? Like this:
"""
tc = TestClient()
tc.save_home({"settings_user.yml": "libc_version: [1, 2, 3]"})
tc.save({"conanfile.py": GenConanfile("dep", "1.0").with_settings("libc_version", "compiler")})
# The extra cppstd and compiler versions are for later demonstrations of combinations of settings
# The cppstd=17 and compiler.version=193 are used thought until the last 2 install calls
tc.run("create . -s=libc_version=2 -s=compiler.cppstd=17")
dep_package_id = tc.created_package_id("dep/1.0")
tc.run("install --requires=dep/1.0 -s=libc_version=3 -s=compiler.cppstd=17", assert_error=True)
# We can't compile, because the dep is not compatible
assert "Missing prebuilt package for 'dep/1.0'" in tc.out

# Let's create a compatibility extensions
libc_compat = textwrap.dedent("""
from conan.tools.scm import Version
def libc_compat(conanfile):
# Do we have the setting?
libc_version = conanfile.settings.get_safe("libc_version")
if libc_version is None:
return []
available_libc_versions = conanfile.settings.libc_version.possible_values()
ret = []
for possible_libc_version in available_libc_versions:
if Version(possible_libc_version) < Version(libc_version):
ret.append({"libc_version": possible_libc_version})
return ret
""")
compat = tc.load_home("extensions/plugins/compatibility/compatibility.py")
compat = "from libc_compat import libc_compat\n" + compat
compat = compat.replace("# Append more factors for your custom compatibility rules here",
"factors.append(libc_compat(conanfile))")
tc.save_home({"extensions/plugins/compatibility/libc_compat.py": libc_compat,
"extensions/plugins/compatibility/compatibility.py": compat})

# Now we try again, this time app will find the compatible dep with libc_version 2
tc.run("install --requires=dep/1.0 -s=libc_version=3 -s=compiler.cppstd=17")
assert f"dep/1.0: Found compatible package '{dep_package_id}'" in tc.out

# And now we try to create the app with libc_version 1, which is still not compatible
tc.run("install --requires=dep/1.0 -s=libc_version=1 -s=compiler.cppstd=17", assert_error=True)
assert "Missing prebuilt package for 'dep/1.0'" in tc.out

# Now we try again, this time app will find the compatible dep with libc_version 2
# And see how we're also compatible over a different cppstd
tc.run("install --requires=dep/1.0 -s=libc_version=3 -s=compiler.cppstd=14")
assert f"dep/1.0: Found compatible package '{dep_package_id}': compiler.cppstd=17, " \
f"libc_version=2" in tc.out

0 comments on commit 5a67746

Please sign in to comment.