Repository: conan-io/conan
Pull request #17796 (2025-02-17 16:14:14)

Never fail a job if coverage decreases
Changelog: omit
Docs: omit


Patch:
diff --git a/codecov.yml b/codecov.yml
index f418c3d13..b3ac3af85 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,3 +1,13 @@
 comment: false # Disable codecov PR comments -> leave only the checks
+coverage:
+    status:
+        project:
+            default:
+                # https://docs.codecov.com/docs/commit-status#informational
+                # Avoids failing on coverage decrease
+                informational: true
+    patch:
+        default:
+            informational: true
 ignore:
   - "test/performance"
diff --git a/conan/cli/commands/lock.py b/conan/cli/commands/lock.py
index 35fe47e6f..55c48e9e1 100644
--- a/conan/cli/commands/lock.py
+++ b/conan/cli/commands/lock.py
@@ -1,3 +1,4 @@
+from collections import defaultdict
 import os
 
 from conan.api.output import ConanOutput
@@ -6,6 +7,7 @@ from conan.cli.command import conan_command, OnceArgument, conan_subcommand
 from conan.cli import make_abs_path
 from conan.cli.args import common_graph_args, validate_common_graph_args
 from conan.cli.printers.graph import print_graph_packages, print_graph_basic
+from conan.errors import ConanException
 from conan.api.model import RecipeReference
 
 
@@ -172,3 +174,63 @@ def lock_update(conan_api, parser, subparser, *args):
     lockfile.update(requires=args.requires, build_requires=args.build_requires,
                     python_requires=args.python_requires, config_requires=args.config_requires)
     conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out)
+
+
+
+
+@conan_subcommand()
+def lock_upgrade(conan_api, parser, subparser, *args):
+    """
+    (Experimental) Upgrade requires, build-requires or python-requires from an existing lockfile given a conanfile
+    or a reference.
+    """
+
+    common_graph_args(subparser)
+    subparser.add_argument('--update-requires', action="append", help='Update requires from lockfile')
+    subparser.add_argument('--update-build-requires', action="append", help='Update build-requires from lockfile')
+    subparser.add_argument('--update-python-requires', action="append", help='Update python-requires from lockfile')
+    subparser.add_argument('--update-config-requires', action="append", help='Update config-requires from lockfile')
+    subparser.add_argument('--build-require', action='store_true', default=False, help='Whether the provided reference is a build-require')
+    args = parser.parse_args(*args)
+
+    # parameter validation
+    validate_common_graph_args(args)
+
+    if not any([args.update_requires, args.update_build_requires, args.update_python_requires, args.update_config_requires]):
+        raise ConanException("At least one of --update-requires, --update-build-requires, "
+                             "--update-python-requires or --update-config-requires should be specified")
+
+    cwd = os.getcwd()
+    path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None
+    remotes = conan_api.remotes.list(args.remote) if not args.no_remote else []
+    overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None
+    lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path,
+                                               cwd=cwd, partial=True, overrides=overrides)
+    profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args)
+
+    # Remove the lockfile entries that will be updated
+    lockfile = conan_api.lockfile.remove_lockfile(lockfile,
+                                                  requires=args.update_requires,
+                                                  python_requires=args.update_python_requires,
+                                                  build_requires=args.update_build_requires,
+                                                  config_requires=args.update_config_requires)
+    # Resolve new graph
+    if path:
+        graph = conan_api.graph.load_graph_consumer(path, args.name, args.version,
+                                                    args.user, args.channel,
+                                                    profile_host, profile_build, lockfile,
+                                                    remotes, args.update,
+                                                    is_build_require=args.build_require)
+    else:
+        graph = conan_api.graph.load_graph_requires(args.requires, args.tool_requires,
+                                                    profile_host, profile_build, lockfile,
+                                                    remotes, args.update)
+    print_graph_basic(graph)
+    graph.report_graph_error()
+    conan_api.graph.analyze_binaries(graph, args.build, remotes=remotes, update=args.update,
+                                     lockfile=lockfile)
+    print_graph_packages(graph)
+
+    lockfile = conan_api.lockfile.update_lockfile(lockfile, graph, args.lockfile_packages,
+                                                  clean=args.lockfile_clean)
+    conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out or "conan.lock")


Test patch:
diff --git a/test/integration/lockfile/test_user_overrides.py b/test/integration/lockfile/test_user_overrides.py
index e3e033969..f41a3a979 100644
--- a/test/integration/lockfile/test_user_overrides.py
+++ b/test/integration/lockfile/test_user_overrides.py
@@ -4,7 +4,7 @@ import textwrap
 import pytest
 
 from conan.test.assets.genconanfile import GenConanfile
-from conan.test.utils.tools import TestClient
+from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient
 
 
 def test_user_overrides():
@@ -360,3 +360,78 @@ class TestLockUpdate:
         lock = c.load("conan.lock")
         assert old not in lock
         assert new in lock
+
+
+
+class TestLockUpgrade:
+    @pytest.mark.parametrize("kind, pkg, old, new", [
+        ("requires", "math", "math/1.0", "math/1.1"),
+        ("build-requires", "cmake", "cmake/1.0", "cmake/1.1"),     # TODO there is not a --build-requires
+        # ("python-requires", "mytool", "mytool/1.0", "mytool/1.1"), # TODO nor a --python-requires
+    ])
+    def test_lock_upgrade(self, kind, pkg, old, new):
+        c = TestClient(light=True)
+        c.save({f"{pkg}/conanfile.py": GenConanfile(pkg)})
+
+        c.run(f"export {pkg} --version=1.0")
+        rev0 = c.exported_recipe_revision()
+        kind_create = "tool-requires" if "build-requires" == kind else kind
+        c.run(f"lock create --{kind_create}={pkg}/[*]")
+        lock = c.load("conan.lock")
+        assert f"{old}#{rev0}" in lock
+
+        c.run(f"export {pkg} --version=1.1")
+        rev1 = c.exported_recipe_revision()
+        c.run(f"lock upgrade --{kind_create}={pkg}/[*] --update-{kind}={pkg}/[*]")
+        lock = c.load("conan.lock")
+        print(lock)
+        assert f"{old}#{rev0}" not in lock
+        assert f"{new}#{rev1}" in lock
+
+
+    def test_lock_upgrade_path(self):
+        c = TestClient(light=True)
+        c.save({"liba/conanfile.py": GenConanfile("liba"),
+                "libb/conanfile.py": GenConanfile("libb"),
+                "libc/conanfile.py": GenConanfile("libc"),
+                "libd/conanfile.py": GenConanfile("libd")})
+        c.run(f"export liba --version=1.0")
+        c.run(f"export libb --version=1.0")
+        c.run(f"export libc --version=1.0")
+        c.run(f"export libd --version=1.0")
+        c.save(
+            {
+                f"conanfile.py": GenConanfile()
+                .with_requires(f"liba/[>=1.0 <2]")
+                .with_requires("libb/[<1.2]")
+                .with_tool_requires("libc/[>=1.0]")
+                .with_python_requires("libd/[>=1.0 <1.2]")
+            }
+        )
+
+        c.run("lock create .")
+        lock = c.load("conan.lock")
+        assert "liba/1.0" in lock
+        assert "libb/1.0" in lock
+        assert "libc/1.0" in lock
+        assert "libd/1.0" in lock
+
+        # Check versions are updated accordingly
+        c.run(f"export liba --version=1.9")
+        c.run(f"export libb --version=1.1")
+        c.run(f"export libb --version=1.2")
+        c.run(f"export libc --version=1.1")
+        c.run(f"export libd --version=1.1")
+        c.run("lock upgrade . --update-requires=liba/1.0 --update-requires=libb/[*] --update-build-requires=libc/[*] --update-python-requires=libd/1.0")
+        lock = c.load("conan.lock")
+        assert "liba/1.9" in lock
+        assert "libb/1.1" in lock
+        assert "libc/1.1" in lock
+        assert "libd/1.1" in lock
+
+        # Check version conanfile version range is respected
+        c.run(f"export libd --version=1.2")
+        c.run("lock upgrade . --update-python-requires=libd/*")
+        lock = c.load("conan.lock")
+        assert "libd/1.1" in lock
+        assert "libd/1.2" not in lock