Repository: conan-io/conan
Pull request #17501 (2024-12-18 12:43:20)

Feature/workspace api
Changelog: Fix: Fix ``Workspace`` when using the ``workspace_api.load()`` and using ``self.run()`` inside ``set_version()``
Docs: Omit

Close https://github.com/conan-io/conan/issues/17306



Patch:
diff --git a/README.md b/README.md
index 7c298f615..70207a0bf 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ Decentralized, open-source (MIT), C/C++ package manager.
 - Docs: https://docs.conan.io
 - Slack: https://cpplang.slack.com (#conan channel. Please, click [here](https://join.slack.com/t/cpplang/shared_invite/zt-1snzdn6rp-rOUxF3166oz1_11Tr5H~xg) to get an invitation)
 - Twitter: https://twitter.com/conan_io
+- Blog: https://blog.conan.io
+- Security reports: https://jfrog.com/trust/report-vulnerability
 
 
 Conan is a package manager for C and C++ developers:
diff --git a/conan/__init__.py b/conan/__init__.py
index 1b584d3ab..93deaa4fe 100644
--- a/conan/__init__.py
+++ b/conan/__init__.py
@@ -1,5 +1,5 @@
 from conans.model.conan_file import ConanFile
 from conan.tools.scm import Version as _Version
 
-__version__ = '2.11.0-dev'
+__version__ = '2.12.0-dev'
 conan_version = _Version(__version__)
diff --git a/conan/api/model.py b/conan/api/model.py
index e0c6fe91d..4c6132bbd 100644
--- a/conan/api/model.py
+++ b/conan/api/model.py
@@ -25,6 +25,7 @@ class Remote:
         self.disabled = disabled
         self.allowed_packages = allowed_packages
         self.remote_type = remote_type
+        self.caching = {}
 
     def __eq__(self, other):
         if other is None:
@@ -45,6 +46,9 @@ class Remote:
     def __repr__(self):
         return str(self)
 
+    def invalidate_cache(self):
+        self.caching = {}
+
 
 class MultiPackagesList:
     def __init__(self):
diff --git a/conan/api/output.py b/conan/api/output.py
index cce692af6..4d110270c 100644
--- a/conan/api/output.py
+++ b/conan/api/output.py
@@ -212,12 +212,12 @@ class ConanOutput:
 
     def trace(self, msg):
         if self._conan_output_level <= LEVEL_TRACE:
-            self._write_message(msg, fg=Color.BRIGHT_WHITE)
+            self._write_message(msg, fg=Color.BLUE)
         return self
 
     def debug(self, msg):
         if self._conan_output_level <= LEVEL_DEBUG:
-            self._write_message(msg)
+            self._write_message(msg, fg=Color.MAGENTA)
         return self
 
     def verbose(self, msg, fg=None, bg=None):
diff --git a/conan/api/subapi/workspace.py b/conan/api/subapi/workspace.py
index 18c33d40c..3c51b7b70 100644
--- a/conan/api/subapi/workspace.py
+++ b/conan/api/subapi/workspace.py
@@ -16,7 +16,7 @@ class WorkspaceAPI:
 
     def __init__(self, conan_api):
         self._conan_api = conan_api
-        self._workspace = Workspace()
+        self._workspace = Workspace(conan_api)
 
     def home_folder(self):
         return self._workspace.home_folder()
diff --git a/conan/cli/commands/profile.py b/conan/cli/commands/profile.py
index c188d9c00..16457a164 100644
--- a/conan/cli/commands/profile.py
+++ b/conan/cli/commands/profile.py
@@ -9,35 +9,44 @@ from conan.errors import ConanException
 from conans.util.files import save
 
 
-def print_profiles(profiles):
-    host, build = profiles
-    cli_out_write("Host profile:")
-    cli_out_write(host.dumps())
-    cli_out_write("Build profile:")
-    cli_out_write(build.dumps())
+def _print_profiles(profiles):
+    if "host" in profiles:
+        ConanOutput().info("Host profile:")
+        cli_out_write(profiles["host"].dumps())
+    if "build" in profiles:
+        ConanOutput().info("Build profile:")
+        cli_out_write(profiles["build"].dumps())
 
 
 def profiles_list_cli_output(profiles):
-    cli_out_write("Profiles found in the cache:")
+    ConanOutput().info("Profiles found in the cache:")
     for p in profiles:
         cli_out_write(p)
 
 
-def json_profiles(profiles):
-    host, build = profiles
-    result = {"host": host.serialize(),
-              "build": build.serialize()}
+def _json_profiles(profiles):
+    result = {}
+    if "host" in profiles:
+        result["host"] = profiles["host"].serialize()
+    if "build" in profiles:
+        result["build"] = profiles["build"].serialize()
     cli_out_write(json.dumps(result))
 
 
-@conan_subcommand(formatters={"text": print_profiles, "json": json_profiles})
+@conan_subcommand(formatters={"text": _print_profiles, "json": _json_profiles})
 def profile_show(conan_api, parser, subparser, *args):
     """
     Show aggregated profiles from the passed arguments.
     """
     add_profiles_args(subparser)
+    subparser.add_argument("-cx", "--context", choices=["host", "build"])
     args = parser.parse_args(*args)
-    result = conan_api.profiles.get_profiles_from_args(args)
+    profiles = conan_api.profiles.get_profiles_from_args(args)
+    result = {}
+    if not args.context or args.context == "host":
+        result["host"] = profiles[0]
+    if not args.context or args.context == "build":
+        result["build"] = profiles[1]
     return result
 
 
diff --git a/conan/internal/workspace.py b/conan/internal/workspace.py
index 74cc1ffbb..5f95ff836 100644
--- a/conan/internal/workspace.py
+++ b/conan/internal/workspace.py
@@ -4,6 +4,8 @@ from pathlib import Path
 import yaml
 
 from conan.api.output import ConanOutput
+from conan.internal.cache.home_paths import HomePaths
+from conan.internal.conan_app import CmdWrapper, ConanFileHelpers
 from conans.client.loader import load_python_file
 from conan.errors import ConanException
 from conans.model.recipe_ref import RecipeReference
@@ -20,13 +22,17 @@ def _find_ws_folder():
 
 
 class _UserWorkspaceAPI:
-    def __init__(self, folder):
+    def __init__(self, folder, conan_api):
         self.folder = folder
+        self._conan_api = conan_api
 
     def load(self, conanfile_path):
         conanfile_path = os.path.join(self.folder, conanfile_path)
         from conans.client.loader import ConanFileLoader
-        loader = ConanFileLoader(pyreq_loader=None, conanfile_helpers=None)
+        cmd_wrap = CmdWrapper(HomePaths(self._conan_api.home_folder).wrapper_path)
+        helpers = ConanFileHelpers(None, cmd_wrap, self._conan_api.config.global_conf,
+                                   cache=None, home_folder=self._conan_api.home_folder)
+        loader = ConanFileLoader(pyreq_loader=None, conanfile_helpers=helpers)
         conanfile = loader.load_named(conanfile_path, name=None, version=None, user=None,
                                       channel=None, remotes=None, graph_lock=None)
         return conanfile
@@ -35,7 +41,8 @@ class _UserWorkspaceAPI:
 class Workspace:
     TEST_ENABLED = False
 
-    def __init__(self):
+    def __init__(self, conan_api):
+        self._conan_api = conan_api
         self._folder = _find_ws_folder()
         if self._folder:
             ConanOutput().warning(f"Workspace found: {self._folder}")
@@ -58,7 +65,7 @@ class Workspace:
             py_file = os.path.join(self._folder, "conanws.py")
             if os.path.exists(py_file):
                 self._py, _ = load_python_file(py_file)
-                setattr(self._py, "workspace_api", _UserWorkspaceAPI(self._folder))
+                setattr(self._py, "workspace_api", _UserWorkspaceAPI(self._folder, conan_api))
                 setattr(self._py, "conanws_data", self._yml)
 
     @property
diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py
index edd570db8..c6f9f6cd9 100644
--- a/conan/tools/env/environment.py
+++ b/conan/tools/env/environment.py
@@ -59,8 +59,8 @@ def environment_wrap_command(conanfile, env_filenames, env_folder, cmd, subsyste
         launchers = " && ".join('"{}"'.format(b) for b in bats)
         if ps1s:
             ps1_launchers = f'{powershell} -Command "' + " ; ".join('&\'{}\''.format(f) for f in ps1s) + '"'
-            cmd = cmd.replace('"', "'")
-            return '{} && {} ; cmd /c {}'.format(launchers, ps1_launchers, cmd)
+            cmd = cmd.replace('"', r'\"')
+            return '{} && {} ; cmd /c "{}"'.format(launchers, ps1_launchers, cmd)
         else:
             return '{} && {}'.format(launchers, cmd)
     elif shs:
@@ -68,8 +68,8 @@ def environment_wrap_command(conanfile, env_filenames, env_folder, cmd, subsyste
         return '{} && {}'.format(launchers, cmd)
     elif ps1s:
         ps1_launchers = f'{powershell} -Command "' + " ; ".join('&\'{}\''.format(f) for f in ps1s) + '"'
-        cmd = cmd.replace('"', "'")
-        return '{} ; cmd /c {}'.format(ps1_launchers, cmd)
+        cmd = cmd.replace('"', r'\"')
+        return '{} ; cmd /c "{}"'.format(ps1_launchers, cmd)
     else:
         return cmd
 
@@ -318,10 +318,9 @@ class Environment:
 
     def vars(self, conanfile, scope="build"):
         """
-        Return an EnvVars object from the current Environment object
         :param conanfile: Instance of a conanfile, usually ``self`` in a recipe
         :param scope: Determine the scope of the declared variables.
-        :return:
+        :return: An EnvVars object from the current Environment object
         """
         return EnvVars(conanfile, self._values, scope)
 
diff --git a/conan/tools/files/files.py b/conan/tools/files/files.py
index bbb96bf90..b3a05026b 100644
--- a/conan/tools/files/files.py
+++ b/conan/tools/files/files.py
@@ -451,6 +451,7 @@ def replace_in_file(conanfile, file_path, search, replace, strict=True, encoding
            string is not found, so nothing is actually replaced.
     :param encoding: (Optional, Defaulted to utf-8): Specifies the input and output files text
            encoding.
+    :return: ``True`` if the pattern was found, ``False`` otherwise if `strict` is ``False``.
     """
     output = conanfile.output
     content = load(conanfile, file_path, encoding=encoding)
@@ -463,6 +464,7 @@ def replace_in_file(conanfile, file_path, search, replace, strict=True, encoding
             return False
     content = content.replace(search, replace)
     save(conanfile, file_path, content, encoding=encoding)
+    return True
 
 
 def collect_libs(conanfile, folder=None):
diff --git a/conan/tools/gnu/gnutoolchain.py b/conan/tools/gnu/gnutoolchain.py
index 336d8e06a..6407f5c30 100644
--- a/conan/tools/gnu/gnutoolchain.py
+++ b/conan/tools/gnu/gnutoolchain.py
@@ -237,7 +237,7 @@ class GnuToolchain:
 
     @staticmethod
     def _dict_to_list(flags):
-        return [f"{k}={v}" if v else k for k, v in flags.items()]
+        return [f"{k}={v}" if v is not None else k for k, v in flags.items()]
 
     @property
     def cxxflags(self):
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index 5e6d21b75..1257b889c 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -187,7 +187,13 @@ class RemoteManager:
             raise
 
     def search_recipes(self, remote, pattern):
-        return self._call_remote(remote, "search", pattern)
+        cached_method = remote.caching.setdefault("search_recipes", {})
+        try:
+            return cached_method[pattern]
+        except KeyError:
+            result = self._call_remote(remote, "search", pattern)
+            cached_method[pattern] = result
+            return result
 
     def search_packages(self, remote, ref):
         packages = self._call_remote(remote, "search_packages", ref)
@@ -234,7 +240,14 @@ class RemoteManager:
                        if k in ("shared", "fPIC", "header_only")]
             if options:
                 headers['Conan-PkgID-Options'] = ';'.join(options)
-        return self._call_remote(remote, "get_latest_package_reference", pref, headers=headers)
+
+        cached_method = remote.caching.setdefault("get_latest_package_reference", {})
+        try:
+            return cached_method[pref]
+        except KeyError:
+            result = self._call_remote(remote, "get_latest_package_reference", pref, headers=headers)
+            cached_method[pref] = result
+            return result
 
     def get_recipe_revision_reference(self, ref, remote) -> bool:
         assert ref.revision is not None, "recipe_exists needs a revision"
diff --git a/conans/client/rest/auth_manager.py b/conans/client/rest/auth_manager.py
index 525de701a..e79fd4040 100644
--- a/conans/client/rest/auth_manager.py
+++ b/conans/client/rest/auth_manager.py
@@ -19,7 +19,7 @@ from conan.errors import ConanException
 LOGIN_RETRIES = 3
 
 
-class RemoteCreds:
+class _RemoteCreds:
     def __init__(self, localdb):
         self._localdb = localdb
 
@@ -28,11 +28,14 @@ class RemoteCreds:
         if creds is None:
             user, token, _ = self._localdb.get_login(remote.url)
             creds = user, token
+            usermsg = f"with user '{user}'" if user else "anonymously"
+            ConanOutput().info(f"Connecting to remote '{remote.name}' {usermsg}")
             setattr(remote, "_creds", creds)
         return creds
 
     def set(self, remote, user, token):
         setattr(remote, "_creds", (user, token))
+        ConanOutput().success(f"Authenticated in remote '{remote.name}' with user '{user}'")
         self._localdb.store(user, token, None, remote.url)
 
 
@@ -40,7 +43,7 @@ class ConanApiAuthManager:
 
     def __init__(self, requester, cache_folder, localdb, global_conf):
         self._requester = requester
-        self._creds = RemoteCreds(localdb)
+        self._creds = _RemoteCreds(localdb)
         self._global_conf = global_conf
         self._cache_folder = cache_folder
 
diff --git a/conans/client/rest/conan_requester.py b/conans/client/rest/conan_requester.py
index edb2ce989..b6700c9f5 100644
--- a/conans/client/rest/conan_requester.py
+++ b/conans/client/rest/conan_requester.py
@@ -9,6 +9,7 @@ import urllib3
 from jinja2 import Template
 from requests.adapters import HTTPAdapter
 
+from conan.api.output import ConanOutput
 from conan.internal.cache.home_paths import HomePaths
 
 from conan import __version__
@@ -198,6 +199,7 @@ class ConanRequester:
             for var_name in ("http_proxy", "https_proxy", "ftp_proxy", "all_proxy", "no_proxy"):
                 popped = True if os.environ.pop(var_name, None) else popped
                 popped = True if os.environ.pop(var_name.upper(), None) else popped
+        ConanOutput(scope="HttpRequest").trace(f"{method}: {url}")
         try:
             all_kwargs = self._add_kwargs(url, kwargs)
             tmp = getattr(self._http_requester, method)(url, **all_kwargs)
diff --git a/conans/model/build_info.py b/conans/model/build_info.py
index 98ea5aae6..57b619dab 100644
--- a/conans/model/build_info.py
+++ b/conans/model/build_info.py
@@ -451,7 +451,7 @@ class _Component:
                 if existing is not None and isinstance(existing, list) and not overwrite:
                     existing.extend(v)
                 else:
-                    current_values[k] = v
+                    current_values[k] = copy.copy(v)
 
     def set_relative_base_folder(self, folder):
         for varname in _DIRS_VAR_NAMES:
diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py
index 27001f20f..ac8418515 100644
--- a/conans/model/conan_file.py
+++ b/conans/model/conan_file.py
@@ -95,6 +95,8 @@ class ConanFile:
             self.generators = [self.generators]
         if isinstance(self.languages, str):
             self.languages = [self.languages]
+        if not all(lang == "C" or lang == "C++" for lang in self.languages):
+            raise ConanException("Only 'C' and 'C++' languages are allowed in 'languages' attribute")
         if isinstance(self.settings, str):
             self.settings = [self.settings]
         self.requires = Requirements(self.requires, self.build_requires, self.test_requires,
diff --git a/conans/requirements_server.txt b/conans/requirements_server.txt
index f7a3f7ab0..d4147ed38 100644
--- a/conans/requirements_server.txt
+++ b/conans/requirements_server.txt
@@ -1,4 +1,4 @@
 # Server
-bottle>=0.12.8, < 0.13
+bottle>=0.12.8, < 0.14
 pluginbase>=0.5
 PyJWT>=2.4.0, <3.0.0


Test patch:
diff --git a/test/functional/test_profile_detect_api.py b/test/functional/test_profile_detect_api.py
index 077092f3d..22ed57578 100644
--- a/test/functional/test_profile_detect_api.py
+++ b/test/functional/test_profile_detect_api.py
@@ -30,11 +30,10 @@ class TestProfileDetectAPI:
             """)
 
         client.save({"profile1": tpl1})
-        client.run("profile show -pr=profile1")
+        client.run("profile show -pr=profile1 --context=host")
         #FIXME: check update setting
         update = str(int(detect_api.detect_msvc_update("194")) % 10)
         expected = textwrap.dedent(f"""\
-            Host profile:
             [settings]
             compiler=msvc
             compiler.cppstd=14
@@ -63,13 +62,12 @@ class TestProfileDetectAPI:
             """)
 
         client.save({"profile1": tpl1})
-        client.run("profile show -pr=profile1")
+        client.run("profile show -pr=profile1 --context=host")
         libc_name, libc_version = detect_api.detect_libc()
         assert libc_name is not None
         assert libc_version is not None
         _, version, _ = detect_api.detect_gcc_compiler()
         expected = textwrap.dedent(f"""\
-            Host profile:
             [settings]
             compiler=gcc
             compiler.version={version}
@@ -93,3 +91,27 @@ class TestProfileDetectAPI:
         client.run("profile show -pr=profile1")
         sdk_version = detect_api.detect_sdk_version(sdk="macosx")
         assert f"os.sdk_version={sdk_version}" in client.out
+
+@pytest.mark.parametrize("context", [None, "host", "build"])
+@pytest.mark.parametrize("f", ["json", "text"])
+def test_profile_show_aggregate_usecase(context, f):
+    tc = TestClient(light=True)
+
+    context_arg = f"--context {context}" if context else ""
+    tc.run(f'profile show {context_arg} -s:h="os=Windows" -s:b="os=Linux" --format={f}')
+
+    if context == "host":
+        if f == "text":
+            assert "Host profile:" not in tc.stdout
+            assert "Host profile:" in tc.stderr
+        assert "Linux" not in tc.out
+    if context == "build":
+        if f == "text":
+            assert "Build profile:" not in tc.stdout
+            assert "Build profile:" in tc.stderr
+        assert "Windows" not in tc.out
+
+    if context in (None, "host"):
+        assert "Windows" in tc.out
+    if context in (None, "build"):
+        assert "Linux" in tc.out
diff --git a/test/functional/toolchains/env/test_virtualenv_powershell.py b/test/functional/toolchains/env/test_virtualenv_powershell.py
index 43d47d6de..db77574b1 100644
--- a/test/functional/toolchains/env/test_virtualenv_powershell.py
+++ b/test/functional/toolchains/env/test_virtualenv_powershell.py
@@ -232,3 +232,22 @@ def test_powershell_deprecated_message(powershell):
         assert "Boolean values for 'tools.env.virtualenv:powershell' are deprecated" in client.out
     else:
         assert "Boolean values for 'tools.env.virtualenv:powershell' are deprecated" not in client.out
+
+
+@pytest.mark.skipif(platform.system() != "Windows", reason="Test for powershell")
+@pytest.mark.parametrize("powershell", [True, "pwsh", "powershell.exe"])
+def test_powershell_quoting(powershell):
+    client = TestClient(path_with_spaces=False)
+    conanfile = textwrap.dedent("""\
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            settings = "os"
+            name = "pkg"
+            version = "0.1"
+            def build(self):
+                self.run('python -c "print(\\'Hello World\\')"', scope="build")
+        """)
+
+    client.save({"conanfile.py": conanfile})
+    client.run(f'create . -c tools.env.virtualenv:powershell={powershell}')
+    assert "Hello World" in client.out
diff --git a/test/integration/command/user_test.py b/test/integration/command/user_test.py
index 66191a621..a5db579f0 100644
--- a/test/integration/command/user_test.py
+++ b/test/integration/command/user_test.py
@@ -430,6 +430,10 @@ class TestRemoteAuth:
                                                             "user", "pass", "user", "pass",
                                                             "user", "pass"])
         c.run("remote auth *")
+        assert "Remote 'default' needs authentication, obtaining credentials" in c.out
+        assert "user: myuser" in c.out
+        c.run("remote auth *")
+        assert "Remote 'default' needs authentication, obtaining credentials" not in c.out
         assert "user: myuser" in c.out
         # Invalidate server secret
         server.test_server.ra.api_v2.credentials_manager.secret = "potato"
@@ -460,3 +464,15 @@ class TestRemoteAuth:
         time.sleep(3)
         c.run("remote auth *")
         assert "error: Too many failed login attempts, bye!" in c.out
+
+    def test_auth_after_logout(self):
+        server = TestServer(users={"myuser": "password"})
+        c = TestClient(servers={"default": server}, inputs=["myuser", "password"]*2)
+        c.run("remote auth *")
+        assert "Remote 'default' needs authentication, obtaining credentials" in c.out
+        assert "user: myuser" in c.out
+        c.run("remote logout *")
+        assert "Changed user of remote 'default' from 'myuser' (authenticated) to 'None'" in c.out
+        c.run("remote auth *")
+        assert "Remote 'default' needs authentication, obtaining credentials" in c.out
+        assert "user: myuser" in c.out
diff --git a/test/integration/command_v2/custom_commands_test.py b/test/integration/command_v2/custom_commands_test.py
index c28b9d50e..cea9c844b 100644
--- a/test/integration/command_v2/custom_commands_test.py
+++ b/test/integration/command_v2/custom_commands_test.py
@@ -419,3 +419,53 @@ class TestCommandAPI:
         c.run("export .")
         c.run("mycommand myremote myurl")
         assert json.loads(c.stdout) == {"myremote": "myurl"}
+
+
+class TestCommandsRemoteCaching:
+    def test_remotes_cache(self):
+        complex_command = textwrap.dedent("""
+            import json
+            from conan.cli.command import conan_command
+            from conan.api.output import ConanOutput
+
+            @conan_command()
+            def mycache(conan_api, parser, *args, **kwargs):
+                \""" this is a command with subcommands \"""
+
+                remotes = conan_api.remotes.list()
+                host = conan_api.profiles.get_profile(["default"])
+                build = conan_api.profiles.get_profile(["default"])
+
+                deps_graph = conan_api.graph.load_graph_requires(["pkg/0.1"], None, host, build,
+                                                                 None, remotes, None, None)
+                conan_api.graph.analyze_binaries(deps_graph, None, remotes=remotes)
+
+                # SECOND RUN!!!
+                # This will break if the caching is not working
+                remotes[0].url = "broken"
+                deps_graph = conan_api.graph.load_graph_requires(["pkg/0.1"], None, host, build,
+                                                                 None, remotes, None, None)
+                conan_api.graph.analyze_binaries(deps_graph, None, remotes=remotes)
+
+                # Now invalidate the cache and see how it breaks
+                try:
+                    remotes[0].invalidate_cache()
+                    deps_graph = conan_api.graph.load_graph_requires(["pkg/0.1"], None, host, build,
+                                                                     None, remotes, None, None)
+                    conan_api.graph.analyze_binaries(deps_graph, None, remotes=remotes)
+                except Exception as e:
+                    ConanOutput().warning(f"Cache invalidated, as expected: {e}")
+            """)
+
+        c = TestClient(default_server_user=True)
+        c.save_home({"extensions/commands/cmd_mycache.py": complex_command})
+
+        c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"),
+                "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_require("dep/0.1")})
+        c.run("create dep")
+        c.run("create pkg")
+        c.run("upload * -r=default -c")
+        c.run("remove * -c")
+        c.run("mycache")
+        # Does not break unexpectedly, caching is working
+        assert "WARN: Cache invalidated, as expected: Invalid URL 'broken" in c.out
diff --git a/test/integration/command_v2/list_test.py b/test/integration/command_v2/list_test.py
index b8bcbed18..32fd98760 100644
--- a/test/integration/command_v2/list_test.py
+++ b/test/integration/command_v2/list_test.py
@@ -724,6 +724,8 @@ class TestListRemotes:
 
     def test_search_no_matching_recipes(self, client):
         expected_output = textwrap.dedent("""\
+        Connecting to remote 'default' with user 'admin'
+        Connecting to remote 'other' with user 'admin'
         Local Cache
           ERROR: Recipe 'whatever/0.1' not found
         default
diff --git a/test/integration/command_v2/search_test.py b/test/integration/command_v2/search_test.py
index 6d6ae26b3..7262f6d81 100644
--- a/test/integration/command_v2/search_test.py
+++ b/test/integration/command_v2/search_test.py
@@ -30,7 +30,9 @@ class TestSearch:
         assert "error: the following arguments are required: reference" in self.client.out
 
     def test_search_no_matching_recipes(self, remotes):
-        expected_output = ("remote1\n"
+        expected_output = ("Connecting to remote 'remote1' anonymously\n"
+                           "Connecting to remote 'remote2' anonymously\n"
+                           "remote1\n"
                            "  ERROR: Recipe 'whatever' not found\n"
                            "remote2\n"
                            "  ERROR: Recipe 'whatever' not found\n")
diff --git a/test/integration/conanfile/conanfile_errors_test.py b/test/integration/conanfile/conanfile_errors_test.py
index 5c59e782d..057589595 100644
--- a/test/integration/conanfile/conanfile_errors_test.py
+++ b/test/integration/conanfile/conanfile_errors_test.py
@@ -240,3 +240,22 @@ def test_consumer_unexpected():
     tc.run("create cmake")
     tc.run("create . -b=missing -pr=profile")
     assert "cmake/1.0 -> True" not in tc.out
+
+
+@pytest.mark.parametrize("languages", [
+    "['C', 'CXX']",
+    "['CXX']",
+    "'CXX'",
+])
+def test_language_unexpected(languages):
+    tc = TestClient(light=True)
+    tc.save({"conanfile.py": GenConanfile().with_class_attribute(f"languages = {languages}")})
+    tc.run("inspect .", assert_error=True)
+    assert "Only 'C' and 'C++' languages are allowed in 'languages' attribute" in tc.out
+
+
+def test_empty_languages():
+    tc = TestClient(light=True)
+    tc.save({"conanfile.py": GenConanfile().with_class_attribute("languages = []")})
+    tc.run("inspect .")
+    assert "Only 'C' and 'C++' languages are allowed in 'languages' attribute" not in tc.out
diff --git a/test/integration/configuration/test_profile_jinja.py b/test/integration/configuration/test_profile_jinja.py
index 5b4a67fa7..a985f992c 100644
--- a/test/integration/configuration/test_profile_jinja.py
+++ b/test/integration/configuration/test_profile_jinja.py
@@ -241,12 +241,11 @@ class TestProfileDetectAPI:
             """)
 
         client.save({"profile1": tpl1})
-        client.run("profile show -pr=profile1")
+        client.run("profile show -pr=profile1 --context=host")
         pr = client.get_default_host_profile()
         the_os = pr.settings['os']
         arch = pr.settings['arch']
         expected = textwrap.dedent(f"""\
-            Host profile:
             [settings]
             arch={arch}
             os={the_os}
diff --git a/test/integration/test_components.py b/test/integration/test_components.py
index 5506fd276..cbe29f118 100644
--- a/test/integration/test_components.py
+++ b/test/integration/test_components.py
@@ -149,3 +149,43 @@ def test_components_overrides():
     # This used to crash, because override was not correctly excluded
     c.run("create app")
     assert "app/0.1: Created package" in c.out
+
+def test_duplication_component_properties():
+    """ Regression for PR 17503 - component lists would be incorrectly aggregated """
+    tc = TestClient(light=True)
+
+    dep = textwrap.dedent("""
+    from conan import ConanFile
+
+    class Dep(ConanFile):
+        name = "dep"
+        version = "0.1"
+
+        def package_info(self):
+            self.cpp_info.components["acomp"].set_property("prop_list", ["value1"])
+            self.cpp_info.components["bcomp"].set_property("prop_list", ["value2"])
+            self.cpp_info.components["ccomp"].set_property("prop_list", ["value3"])
+    """)
+
+    conanfile = textwrap.dedent("""
+    from conan import ConanFile
+
+    class Pkg(ConanFile):
+        name = "pkg"
+        version = "0.1"
+        requires = "dep/0.1"
+
+        def generate(self):
+            # Calling this would break property lists of the last lex sorted component
+            aggregated_components = self.dependencies["dep"].cpp_info.aggregated_components()
+            ccomp = self.dependencies["dep"].cpp_info.components["ccomp"]
+            self.output.info("ccomp list: " + str(ccomp.get_property("prop_list")))
+    """)
+
+    tc.save({"dep/conanfile.py": dep,
+             "conanfile.py": conanfile})
+    tc.run("create dep")
+    tc.run("create .")
+    # The bug would give ccomp the prop_list values of the other two components
+    assert "pkg/0.1: ccomp list: ['value3', 'value2', 'value1']" not in tc.out
+    assert "pkg/0.1: ccomp list: ['value3']" in tc.out
diff --git a/test/integration/workspace/test_workspace.py b/test/integration/workspace/test_workspace.py
index fc78c0847..2dbb50917 100644
--- a/test/integration/workspace/test_workspace.py
+++ b/test/integration/workspace/test_workspace.py
@@ -192,6 +192,35 @@ class TestAddRemove:
             # Doesn't fail
             assert "other/14.5 - Editable" in c.out
 
+    def test_api_dynamic_version_run(self):
+        # https://github.com/conan-io/conan/issues/17306
+        c = TestClient(light=True)
+        conanfile = textwrap.dedent("""
+            from io import StringIO
+            from conan import ConanFile
+            class Lib(ConanFile):
+                name= "pkg"
+                def set_version(self):
+                    my_buf = StringIO()
+                    self.run('echo 2.1', stdout=my_buf)
+                    self.version = my_buf.getvalue().strip()
+            """)
+
+        workspace = textwrap.dedent("""\
+           import os
+           name = "myws"
+
+           def editables(*args, **kwargs):
+               conanfile = workspace_api.load("dep1/conanfile.py")
+               return {f"{conanfile.name}/{conanfile.version}": {"path": "dep1/conanfile.py"}}
+            """)
+
+        c.save({"conanws.py": workspace,
+                "dep1/conanfile.py": conanfile})
+        c.run("workspace info --format=json")
+        info = json.loads(c.stdout)
+        assert info["editables"] == {"pkg/2.1": {"path": "dep1/conanfile.py"}}
+
     def test_error_uppercase(self):
         c = TestClient(light=True)
         c.save({"conanws.py": "name='myws'",
diff --git a/test/unittests/tools/files/test_file_read_and_write.py b/test/unittests/tools/files/test_file_read_and_write.py
index 4a6d12ab1..1fee30fbe 100644
--- a/test/unittests/tools/files/test_file_read_and_write.py
+++ b/test/unittests/tools/files/test_file_read_and_write.py
@@ -3,6 +3,9 @@
 
 import os
 
+import pytest
+
+from conan.errors import ConanException
 from conan.tools.files import replace_in_file, save, load
 from conan.test.utils.mocks import ConanFileMock
 from conan.test.utils.test_files import temp_folder
@@ -36,18 +39,18 @@ def test_replace_in_file():
 
     # By default utf-8 is used
     save(conanfile, file_path, "你很重，伙計")
-    replace_in_file(conanfile, file_path, "重", "0")
+    assert replace_in_file(conanfile, file_path, "重", "0")
     contents = load(conanfile, file_path)
     assert contents == "你很0，伙計"
 
     # Replacing with other encodings is also possible
     save(conanfile, file_path, "Ö¼", encoding="cp1252")
-    replace_in_file(conanfile, file_path, "¼", "0", encoding="cp1252")
+    assert replace_in_file(conanfile, file_path, "¼", "0", encoding="cp1252")
     contents = load(conanfile, file_path, encoding="cp1252")
     assert contents == "Ö0"
 
     save(conanfile, file_path, "Ö¼", encoding="ISO-8859-1")
-    replace_in_file(conanfile, file_path, "¼", "0", encoding="ISO-8859-1")
+    assert replace_in_file(conanfile, file_path, "¼", "0", encoding="ISO-8859-1")
     contents = load(conanfile, file_path, encoding="ISO-8859-1")
     assert contents == "Ö0"
 
@@ -57,3 +60,9 @@ def test_replace_in_file():
     replace_in_file(conanfile, file_path, "重", "0", encoding="utf-16")
     contents = load(conanfile, file_path, encoding="utf-16")
     assert contents == "你很0，伙計"
+
+    with pytest.raises(ConanException, match="didn't find pattern"):
+        replace_in_file(conanfile, file_path, "not existing", "0", encoding="utf-16")
+
+    assert not replace_in_file(conanfile, file_path, "not existing", "0",
+                               encoding="utf-16", strict=False)
diff --git a/test/unittests/tools/gnu/test_gnutoolchain.py b/test/unittests/tools/gnu/test_gnutoolchain.py
index cf053530e..c53f9d7bb 100644
--- a/test/unittests/tools/gnu/test_gnutoolchain.py
+++ b/test/unittests/tools/gnu/test_gnutoolchain.py
@@ -192,5 +192,9 @@ def test_update_or_prune_any_args(cross_building_conanfile):
     assert "'--force" not in new_autoreconf_args
     # Add new value to make_args
     at.make_args.update({"--new-complex-flag": "new-value"})
+    at.make_args.update({"--new-empty-flag": ""})
+    at.make_args.update({"--new-no-value-flag": None})
     new_make_args = cmd_args_to_string(GnuToolchain._dict_to_list(at.make_args))
     assert "--new-complex-flag=new-value" in new_make_args
+    assert "--new-empty-flag=" in new_make_args
+    assert "--new-no-value-flag" in new_make_args and "--new-no-value-flag=" not in new_make_args