Repository: conan-io/conan
Pull request #12846 (2023-01-04 20:46:16)

Also write CMake Test Presets
Changelog: (Feature): Also write CMake Test Presets
Docs: https://github.com/conan-io/docs/pull/2889

Right now the `CMakeToolchain` generator only writes configure and build presets. That requires some guesswork to find the build directory in case the `cmake_layout` is used.

My testcase:
```python
from conan import ConanFile
from conan.tools.cmake import cmake_layout

class Pkg(ConanFile):
    settings = "os", "arch", "compiler", "build_type"
    generators = "CMakeToolchain"

    def layout(self):
    	cmake_layout(self)
```

```cmake
cmake_minimum_required(VERSION 3.15)
project(testcase NONE)
enable_testing()
add_test(NAME some_test COMMAND ${CMAKE_COMMAND} -E true)
```

```shell
conan install . 
cmake --preset release
cmake --build --preset release
ctest --preset release # this fails right now but succeeds with this PR
```

If you welcome adding test presets I'll extend add the necessary docs repo PR.

Thanks,
Gregor

- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [x] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one.

<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>


CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/conan-io/conan?pullRequest=12846) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/conan-io/conan?pullRequest=12846) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/conan-io/conan?pullRequest=12846) it.</sub>

Patch:
diff --git a/conan/tools/apple/xcodedeps.py b/conan/tools/apple/xcodedeps.py
index fb79a0259..f4b6d9209 100644
--- a/conan/tools/apple/xcodedeps.py
+++ b/conan/tools/apple/xcodedeps.py
@@ -130,6 +130,7 @@ class XcodeDeps(object):
             merged = [var for cpp_info in transitive_cpp_infos for var in getattr(cpp_info, name)]
             return list(OrderedDict.fromkeys(merged).keys())
 
+        # TODO: Investigate if paths can be made relative to "root" folder
         fields = {
             'pkg_name': pkg_name,
             'comp_name': comp_name,
@@ -270,8 +271,10 @@ class XcodeDeps(object):
                     transitive_internal = list(OrderedDict.fromkeys(transitive_internal).keys())
                     transitive_external = list(OrderedDict.fromkeys(transitive_external).keys())
 
+                    # In case dep is editable and package_folder=None
+                    pkg_folder = dep.package_folder or dep.recipe_folder
                     component_content = self.get_content_for_component(dep_name, comp_name,
-                                                                       dep.package_folder,
+                                                                       pkg_folder,
                                                                        transitive_internal,
                                                                        transitive_external)
                     include_components_names.append((dep_name, comp_name))
@@ -289,7 +292,9 @@ class XcodeDeps(object):
                         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
-                root_content = self.get_content_for_component(dep_name, dep_name, dep.package_folder, [dep.cpp_info],
+                # 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(dep_name, dep_name, pkg_folder, [dep.cpp_info],
                                                               required_components)
                 include_components_names.append((dep_name, dep_name))
                 result.update(root_content)
diff --git a/conan/tools/cmake/layout.py b/conan/tools/cmake/layout.py
index f39ef6974..abddc047f 100644
--- a/conan/tools/cmake/layout.py
+++ b/conan/tools/cmake/layout.py
@@ -22,20 +22,21 @@ def cmake_layout(conanfile, generator=None, src_folder=".", build_folder="build"
         raise ConanException("'build_type' setting not defined, it is necessary for cmake_layout()")
 
     build_folder = build_folder if not subproject else os.path.join(subproject, build_folder)
-    custom_conf = get_build_folder_custom_vars(conanfile)
-    if custom_conf:
-        build_folder = os.path.join(build_folder, custom_conf)
+    config_build_folder, user_defined_build = get_build_folder_custom_vars(conanfile)
 
-    if multi:
-        conanfile.folders.build = build_folder
+    if config_build_folder:
+        conanfile.folders.build = os.path.join(build_folder, config_build_folder)
     else:
-        conanfile.folders.build = os.path.join(build_folder, build_type)
+        if not multi and not user_defined_build:
+            conanfile.folders.build = os.path.join(build_folder, build_type)
+        else:
+            conanfile.folders.build = build_folder
 
-    conanfile.folders.generators = os.path.join(build_folder, "generators")
+    conanfile.folders.generators = os.path.join(conanfile.folders.build, "generators")
 
     conanfile.cpp.source.includedirs = ["include"]
 
-    if multi:
+    if multi and not user_defined_build:
         conanfile.cpp.build.libdirs = ["{}".format(build_type)]
         conanfile.cpp.build.bindirs = ["{}".format(build_type)]
     else:
@@ -49,17 +50,11 @@ def get_build_folder_custom_vars(conanfile):
                                     default=[], check_type=list)
     ret = []
     for s in build_vars:
+        group, var = s.split(".", 1)
         tmp = None
-        if s.startswith("settings."):
-            _, var = s.split("settings.", 1)
-            if var == "build_type":
-                raise ConanException("Error, don't include 'settings.build_type' in the "
-                                     "'tools.cmake.cmake_layout:build_folder_vars' conf. It is "
-                                     "managed by default because 'CMakeToolchain' and 'CMakeDeps' "
-                                     "are multi-config generators.`")
+        if group == "settings":
             tmp = conanfile.settings.get_safe(var)
-        elif s.startswith("options."):
-            _, var = s.split("options.", 1)
+        elif group == "options":
             value = conanfile.options.get_safe(var)
             if value is not None:
                 tmp = "{}_{}".format(var, value)
@@ -69,4 +64,5 @@ def get_build_folder_custom_vars(conanfile):
         if tmp:
             ret.append(tmp.lower())
 
-    return "-".join(ret)
+    user_defined_build = "settings.build_type" in build_vars
+    return "-".join(ret), user_defined_build
diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py
index 6d4a9a735..306b811d3 100644
--- a/conan/tools/cmake/presets.py
+++ b/conan/tools/cmake/presets.py
@@ -10,18 +10,29 @@ from conans.util.files import save, load
 
 
 def _build_preset(conanfile, multiconfig):
+    return _common_build_and_test_preset_fields(conanfile, multiconfig)
+
+
+def _test_preset(conanfile, multiconfig):
+    return _common_build_and_test_preset_fields(conanfile, multiconfig)
+
+
+def _common_build_and_test_preset_fields(conanfile, multiconfig):
     build_type = conanfile.settings.get_safe("build_type")
     configure_preset_name = _configure_preset_name(conanfile, multiconfig)
-    ret = {"name": _build_preset_name(conanfile),
+    ret = {"name": _build_and_test_preset_name(conanfile),
            "configurePreset": configure_preset_name}
     if multiconfig:
         ret["configuration"] = build_type
     return ret
 
 
-def _build_preset_name(conanfile):
+def _build_and_test_preset_name(conanfile):
     build_type = conanfile.settings.get_safe("build_type")
-    custom_conf = get_build_folder_custom_vars(conanfile)
+    custom_conf, user_defined_build = get_build_folder_custom_vars(conanfile)
+    if user_defined_build:
+        return custom_conf
+
     if custom_conf:
         if build_type:
             return "{}-{}".format(custom_conf, build_type.lower())
@@ -32,7 +43,10 @@ def _build_preset_name(conanfile):
 
 def _configure_preset_name(conanfile, multiconfig):
     build_type = conanfile.settings.get_safe("build_type")
-    custom_conf = get_build_folder_custom_vars(conanfile)
+    custom_conf, user_defined_build = get_build_folder_custom_vars(conanfile)
+
+    if user_defined_build:
+        return custom_conf
 
     if multiconfig or not build_type:
         return "default" if not custom_conf else custom_conf
@@ -102,6 +116,14 @@ def _configure_preset(conanfile, generator, cache_variables, toolchain_file, mul
     return ret
 
 
+def _insert_preset(data, preset_name, preset):
+    position = _get_already_existing_preset_index(preset["name"], data[preset_name])
+    if position is not None:
+        data[preset_name][position] = preset
+    else:
+        data[preset_name].append(preset)
+
+
 def _forced_schema_2(conanfile):
     version = conanfile.conf.get("tools.cmake.cmaketoolchain.presets:max_schema_version",
                                  check_type=int)
@@ -137,6 +159,7 @@ def _contents(conanfile, toolchain_file, cache_variables, generator):
           }
     multiconfig = is_multi_configuration(generator)
     ret["buildPresets"].append(_build_preset(conanfile, multiconfig))
+    ret["testPresets"].append(_test_preset(conanfile, multiconfig))
     _conf = _configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig)
     ret["configurePresets"].append(_conf)
     return ret
@@ -168,20 +191,14 @@ def write_cmake_presets(conanfile, toolchain_file, generator, cache_variables,
     if os.path.exists(preset_path):
         data = json.loads(load(preset_path))
         build_preset = _build_preset(conanfile, multiconfig)
-        position = _get_already_existing_preset_index(build_preset["name"], data["buildPresets"])
-        if position is not None:
-            data["buildPresets"][position] = build_preset
-        else:
-            data["buildPresets"].append(build_preset)
+        _insert_preset(data, "buildPresets", build_preset)
+
+        test_preset = _test_preset(conanfile, multiconfig)
+        _insert_preset(data, "testPresets", test_preset)
 
         configure_preset = _configure_preset(conanfile, generator, cache_variables, toolchain_file,
                                              multiconfig)
-        position = _get_already_existing_preset_index(configure_preset["name"],
-                                                      data["configurePresets"])
-        if position is not None:
-            data["configurePresets"][position] = configure_preset
-        else:
-            data["configurePresets"].append(configure_preset)
+        _insert_preset(data, "configurePresets", configure_preset)
     else:
         data = _contents(conanfile, toolchain_file, cache_variables, generator)
 


Test patch:
diff --git a/conans/test/functional/layout/test_build_system_layout_helpers.py b/conans/test/functional/layout/test_build_system_layout_helpers.py
index 4bd7c8cd4..c683114b8 100644
--- a/conans/test/functional/layout/test_build_system_layout_helpers.py
+++ b/conans/test/functional/layout/test_build_system_layout_helpers.py
@@ -168,7 +168,8 @@ def test_cmake_layout_external_sources():
 
     # Local flow
     client.run("install . foo/1.0 -s os=Linux")
-    assert os.path.exists(os.path.join(client.current_folder, "build", "generators", "generate.txt"))
+    assert os.path.exists(os.path.join(client.current_folder,
+                                       "build", "Release", "generators", "generate.txt"))
     client.run("source .")
     assert os.path.exists(os.path.join(client.current_folder, "src", "source.txt"))
     client.run("build .")
@@ -294,4 +295,4 @@ def test_cmake_layout_custom_build_folder():
     client.save({"conanfile.py": conanfile})
     client.run("install .")
     assert os.path.exists(os.path.join(client.current_folder,
-                                       "mybuild/generators/conan_toolchain.cmake"))
+                                       "mybuild/Release/generators/conan_toolchain.cmake"))
diff --git a/conans/test/functional/layout/test_local_commands.py b/conans/test/functional/layout/test_local_commands.py
index bd036d617..0c07d5cdd 100644
--- a/conans/test/functional/layout/test_local_commands.py
+++ b/conans/test/functional/layout/test_local_commands.py
@@ -372,7 +372,11 @@ def test_start_dir_failure():
     c = TestClient()
     c.save(pkg_cmake("dep", "0.1"))
     c.run("install .")
-    expected_path = os.path.join(c.current_folder, "build", "generators", "conan_toolchain.cmake")
+    if platform.system() != "Windows":
+        gen_folder = os.path.join(c.current_folder, "build", "Release", "generators")
+    else:
+        gen_folder = os.path.join(c.current_folder, "build", "generators")
+    expected_path = os.path.join(gen_folder, "conan_toolchain.cmake")
     assert os.path.exists(expected_path)
     os.unlink(expected_path)
     with c.chdir("build"):
diff --git a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py
index 45b5d1d97..c1ceb5d3a 100644
--- a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py
+++ b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py
@@ -497,7 +497,7 @@ def test_system_dep():
     files = pkg_cmake("mylib", "0.1", requires=["zlib/0.1"])
     files["CMakeLists.txt"] = files["CMakeLists.txt"].replace("find_package(zlib)",
                                                               "find_package(ZLIB)")
-    files["CMakeLists.txt"] = files["CMakeLists.txt"].replace("zlib::zlib","ZLIB::ZLIB")
+    files["CMakeLists.txt"] = files["CMakeLists.txt"].replace("zlib::zlib", "ZLIB::ZLIB")
     client.save({os.path.join("mylib", name): content for name, content in files.items()})
     files = pkg_cmake("consumer", "0.1", requires=["mylib/0.1"])
     client.save({os.path.join("consumer", name): content for name, content in files.items()})
@@ -509,7 +509,7 @@ def test_system_dep():
     client.run("install consumer")
     if platform.system() != "Windows":
         host_arch = client.get_default_host_profile().settings['arch']
-        data = os.path.join(f"consumer/build/generators/mylib-release-{host_arch}-data.cmake")
+        data = f"consumer/build/Release/generators/mylib-release-{host_arch}-data.cmake"
         contents = client.load(data)
         assert 'set(ZLIB_FIND_MODE "")' in contents
 
diff --git a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py
index e03007054..06c6d1943 100644
--- a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py
+++ b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py
@@ -52,7 +52,8 @@ def test_shared_link_flags():
     client.run("new hello/1.0 -m cmake_lib")
     client.save({"conanfile.py": conanfile})
     client.run("create .")
-    t = os.path.join("test_package", "build", "generators", "hello-release-x86_64-data.cmake")
+    t = os.path.join("test_package", "build", "Release",
+                     "generators", "hello-release-x86_64-data.cmake")
     target_data_cmake_content = client.load(t)
     assert 'set(hello_SHARED_LINK_FLAGS_RELEASE "-z now;-z relro")' in target_data_cmake_content
     assert 'set(hello_EXE_LINK_FLAGS_RELEASE "-z now;-z relro")' in target_data_cmake_content
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
index 49f73cf5a..e172d3927 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -297,8 +297,14 @@ def test_install_output_directories():
     p_folder = client.cache.package_layout(pref.ref).package(pref)
     assert os.path.exists(os.path.join(p_folder, "mylibs"))
     assert not os.path.exists(os.path.join(p_folder, "lib"))
+
     b_folder = client.cache.package_layout(pref.ref).build(pref)
-    toolchain = client.load(os.path.join(b_folder, "build", "generators", "conan_toolchain.cmake"))
+    if platform.system() != "Windows":
+        gen_folder = os.path.join(b_folder, "build", "Release", "generators")
+    else:
+        gen_folder = os.path.join(b_folder, "build", "generators")
+
+    toolchain = client.load(os.path.join(gen_folder, "conan_toolchain.cmake"))
     assert 'set(CMAKE_INSTALL_LIBDIR "mylibs")' in toolchain
 
 
@@ -588,9 +594,12 @@ def test_cmake_presets_multiple_settings_single_config():
     presets = json.loads(load(user_presets["include"][0]))
     assert len(presets["configurePresets"]) == 1
     assert len(presets["buildPresets"]) == 1
+    assert len(presets["testPresets"]) == 1
     assert presets["configurePresets"][0]["name"] == "apple-clang-12.0-gnu17-release"
     assert presets["buildPresets"][0]["name"] == "apple-clang-12.0-gnu17-release"
     assert presets["buildPresets"][0]["configurePreset"] == "apple-clang-12.0-gnu17-release"
+    assert presets["testPresets"][0]["name"] == "apple-clang-12.0-gnu17-release"
+    assert presets["testPresets"][0]["configurePreset"] == "apple-clang-12.0-gnu17-release"
 
     # If we create the "Debug" one, it has the same toolchain and preset file, that is
     # always multiconfig
@@ -602,12 +611,17 @@ def test_cmake_presets_multiple_settings_single_config():
     presets = json.loads(load(user_presets["include"][0]))
     assert len(presets["configurePresets"]) == 2
     assert len(presets["buildPresets"]) == 2
+    assert len(presets["testPresets"]) == 2
     assert presets["configurePresets"][0]["name"] == "apple-clang-12.0-gnu17-release"
     assert presets["configurePresets"][1]["name"] == "apple-clang-12.0-gnu17-debug"
     assert presets["buildPresets"][0]["name"] == "apple-clang-12.0-gnu17-release"
     assert presets["buildPresets"][1]["name"] == "apple-clang-12.0-gnu17-debug"
     assert presets["buildPresets"][0]["configurePreset"] == "apple-clang-12.0-gnu17-release"
     assert presets["buildPresets"][1]["configurePreset"] == "apple-clang-12.0-gnu17-debug"
+    assert presets["testPresets"][0]["name"] == "apple-clang-12.0-gnu17-release"
+    assert presets["testPresets"][1]["name"] == "apple-clang-12.0-gnu17-debug"
+    assert presets["testPresets"][0]["configurePreset"] == "apple-clang-12.0-gnu17-release"
+    assert presets["testPresets"][1]["configurePreset"] == "apple-clang-12.0-gnu17-debug"
 
     # But If we change, for example, the cppstd and the compiler version, the toolchain
     # and presets will be different, but it will be appended to the UserPresets.json
@@ -623,26 +637,32 @@ def test_cmake_presets_multiple_settings_single_config():
     presets = json.loads(load(user_presets["include"][1]))
     assert len(presets["configurePresets"]) == 1
     assert len(presets["buildPresets"]) == 1
+    assert len(presets["testPresets"]) == 1
     assert presets["configurePresets"][0]["name"] == "apple-clang-13-gnu20-release"
     assert presets["buildPresets"][0]["name"] == "apple-clang-13-gnu20-release"
     assert presets["buildPresets"][0]["configurePreset"] == "apple-clang-13-gnu20-release"
+    assert presets["testPresets"][0]["name"] == "apple-clang-13-gnu20-release"
+    assert presets["testPresets"][0]["configurePreset"] == "apple-clang-13-gnu20-release"
 
     # We can build with cmake manually
     if platform.system() == "Darwin":
         client.run_command("cmake . --preset apple-clang-12.0-gnu17-release")
         client.run_command("cmake --build --preset apple-clang-12.0-gnu17-release")
+        client.run_command("ctest --preset apple-clang-12.0-gnu17-release")
         client.run_command("./build/apple-clang-12.0-gnu17/Release/hello")
         assert "Hello World Release!" in client.out
         assert "__cplusplus2017" in client.out
 
         client.run_command("cmake . --preset apple-clang-12.0-gnu17-debug")
         client.run_command("cmake --build --preset apple-clang-12.0-gnu17-debug")
+        client.run_command("ctest --preset apple-clang-12.0-gnu17-debug")
         client.run_command("./build/apple-clang-12.0-gnu17/Debug/hello")
         assert "Hello World Debug!" in client.out
         assert "__cplusplus2017" in client.out
 
         client.run_command("cmake . --preset apple-clang-13-gnu20-release")
         client.run_command("cmake --build --preset apple-clang-13-gnu20-release")
+        client.run_command("ctest --preset apple-clang-13-gnu20-release")
         client.run_command("./build/apple-clang-13-gnu20/Release/hello")
         assert "Hello World Release!" in client.out
         assert "__cplusplus2020" in client.out
@@ -666,6 +686,7 @@ def test_cmake_presets_duplicated_install(multiconfig):
     assert os.path.exists(presets_path)
     contents = json.loads(load(presets_path))
     assert len(contents["buildPresets"]) == 1
+    assert len(contents["testPresets"]) == 1
 
 
 def test_remove_missing_presets():
@@ -730,6 +751,7 @@ def test_cmake_presets_options_single_config():
             shared_str = "shared_true" if shared else "shared_false"
             client.run_command("cmake . --preset apple-clang-{}-release".format(shared_str))
             client.run_command("cmake --build --preset apple-clang-{}-release".format(shared_str))
+            client.run_command("ctest --preset apple-clang-{}-release".format(shared_str))
             the_lib = "libhello.a" if not shared else "libhello.dylib"
             path = os.path.join(client.current_folder,
                                 "build", "apple-clang-{}".format(shared_str), "release", the_lib)
@@ -757,9 +779,12 @@ def test_cmake_presets_multiple_settings_multi_config():
     presets = json.loads(load(user_presets["include"][0]))
     assert len(presets["configurePresets"]) == 1
     assert len(presets["buildPresets"]) == 1
+    assert len(presets["testPresets"]) == 1
     assert presets["configurePresets"][0]["name"] == "dynamic-14"
     assert presets["buildPresets"][0]["name"] == "dynamic-14-release"
     assert presets["buildPresets"][0]["configurePreset"] == "dynamic-14"
+    assert presets["testPresets"][0]["name"] == "dynamic-14-release"
+    assert presets["testPresets"][0]["configurePreset"] == "dynamic-14"
 
     # If we create the "Debug" one, it has the same toolchain and preset file, that is
     # always multiconfig
@@ -771,11 +796,16 @@ def test_cmake_presets_multiple_settings_multi_config():
     presets = json.loads(load(user_presets["include"][0]))
     assert len(presets["configurePresets"]) == 1
     assert len(presets["buildPresets"]) == 2
+    assert len(presets["testPresets"]) == 2
     assert presets["configurePresets"][0]["name"] == "dynamic-14"
     assert presets["buildPresets"][0]["name"] == "dynamic-14-release"
     assert presets["buildPresets"][1]["name"] == "dynamic-14-debug"
     assert presets["buildPresets"][0]["configurePreset"] == "dynamic-14"
     assert presets["buildPresets"][1]["configurePreset"] == "dynamic-14"
+    assert presets["testPresets"][0]["name"] == "dynamic-14-release"
+    assert presets["testPresets"][1]["name"] == "dynamic-14-debug"
+    assert presets["testPresets"][0]["configurePreset"] == "dynamic-14"
+    assert presets["testPresets"][1]["configurePreset"] == "dynamic-14"
 
     # But If we change, for example, the cppstd and the compiler version, the toolchain
     # and presets will be different, but it will be appended to the UserPresets.json
@@ -790,19 +820,24 @@ def test_cmake_presets_multiple_settings_multi_config():
     presets = json.loads(load(user_presets["include"][1]))
     assert len(presets["configurePresets"]) == 1
     assert len(presets["buildPresets"]) == 1
+    assert len(presets["testPresets"]) == 1
     assert presets["configurePresets"][0]["name"] == "static-17"
     assert presets["buildPresets"][0]["name"] == "static-17-release"
     assert presets["buildPresets"][0]["configurePreset"] == "static-17"
+    assert presets["testPresets"][0]["name"] == "static-17-release"
+    assert presets["testPresets"][0]["configurePreset"] == "static-17"
 
     # We can build with cmake manually
     client.run_command("cmake . --preset dynamic-14")
 
     client.run_command("cmake --build --preset dynamic-14-release")
+    client.run_command("ctest --preset dynamic-14-release")
     client.run_command("build\\dynamic-14\\Release\\hello")
     assert "Hello World Release!" in client.out
     assert "MSVC_LANG2014" in client.out
 
     client.run_command("cmake --build --preset dynamic-14-debug")
+    client.run_command("ctest --preset dynamic-14-debug")
     client.run_command("build\\dynamic-14\\Debug\\hello")
     assert "Hello World Debug!" in client.out
     assert "MSVC_LANG2014" in client.out
@@ -810,6 +845,7 @@ def test_cmake_presets_multiple_settings_multi_config():
     client.run_command("cmake . --preset static-17")
 
     client.run_command("cmake --build --preset static-17-release")
+    client.run_command("ctest --preset static-17-release")
     client.run_command("build\\static-17\\Release\\hello")
     assert "Hello World Release!" in client.out
     assert "MSVC_LANG2017" in client.out
@@ -837,10 +873,12 @@ def test_user_presets_version2():
     if platform.system() == "Windows":
         client.run_command("cmake . --preset 14")
         client.run_command("cmake --build --preset 14-release")
+        client.run_command("ctest --preset 14-release")
         client.run_command(r"build\14\Release\hello.exe")
     else:
         client.run_command("cmake . --preset 14-release")
         client.run_command("cmake --build --preset 14-release")
+        client.run_command("ctest --preset 14-release")
         client.run_command("./build/14/Release/hello")
 
     assert "Hello World Release!" in client.out
@@ -853,10 +891,12 @@ def test_user_presets_version2():
     if platform.system() == "Windows":
         client.run_command("cmake . --preset 17")
         client.run_command("cmake --build --preset 17-release")
+        client.run_command("ctest --preset 17-release")
         client.run_command(r"build\17\Release\hello.exe")
     else:
         client.run_command("cmake . --preset 17-release")
         client.run_command("cmake --build --preset 17-release")
+        client.run_command("ctest --preset 17-release")
         client.run_command("./build/17/Release/hello")
 
     assert "Hello World Release!" in client.out
@@ -943,10 +983,12 @@ def test_cmake_presets_with_conanfile_txt():
     if platform.system() != "Windows":
         c.run_command("cmake --preset debug")
         c.run_command("cmake --build --preset debug")
+        c.run_command("ctest --preset debug")
         c.run_command("./build/Debug/foo")
     else:
         c.run_command("cmake --preset default")
         c.run_command("cmake --build --preset debug")
+        c.run_command("ctest --preset debug")
         c.run_command("build\\Debug\\foo")
 
     assert "Hello World Debug!" in c.out
@@ -954,23 +996,25 @@ def test_cmake_presets_with_conanfile_txt():
     if platform.system() != "Windows":
         c.run_command("cmake --preset release")
         c.run_command("cmake --build --preset release")
+        c.run_command("ctest --preset release")
         c.run_command("./build/Release/foo")
     else:
         c.run_command("cmake --build --preset release")
+        c.run_command("ctest --preset release")
         c.run_command("build\\Release\\foo")
 
     assert "Hello World Release!" in c.out
 
 
-def test_cmake_presets_forbidden_build_type():
+def test_cmake_presets_not_forbidden_build_type():
     client = TestClient(path_with_spaces=False)
     client.run("new hello/0.1 --template cmake_exe")
     # client.run("new cmake_exe -d name=hello -d version=0.1")
     settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=' \
                       '\'["options.missing", "settings.build_type"]\''
-    client.run("install . {}".format(settings_layout), assert_error=True)
-    assert "Error, don't include 'settings.build_type' in the " \
-           "'tools.cmake.cmake_layout:build_folder_vars' conf" in client.out
+    client.run("install . {}".format(settings_layout))
+    assert os.path.exists(os.path.join(client.current_folder,
+                                       "build/release/generators/conan_toolchain.cmake"))
 
 
 def test_resdirs_cmake_install():
@@ -1116,7 +1160,8 @@ def test_cmake_toolchain_vars_when_option_declared():
     # the CMakeLists
     fpic_option = "-o mylib:fPIC=False" if platform.system() != "Windows" else ""
     t.run(f"install . -o mylib:shared=False {fpic_option}")
-    t.run_command("cmake -S . -B build/ -DCMAKE_TOOLCHAIN_FILE=build/generators/conan_toolchain.cmake")
+    folder = "build/generators" if platform.system() == "Windows" else "build/Release/generators"
+    t.run_command(f"cmake -S . -B build/ -DCMAKE_TOOLCHAIN_FILE={folder}/conan_toolchain.cmake")
     assert "mylib target type: STATIC_LIBRARY" in t.out
     assert f"mylib position independent code: OFF" in t.out
 
@@ -1185,8 +1230,6 @@ def test_find_program_for_tool_requires():
                 "build_profile": build_profile
                 })
 
-    xxx = client.get_default_build_profile()
-
     client.run("create . -pr:b build_profile -pr:h build_profile")
     client.run("create . -pr:b build_profile -pr:h host_profile")
 
@@ -1226,7 +1269,7 @@ def test_find_program_for_tool_requires():
     host_context_package_id = "bf544cd3bc20b82121fd76b82eacbb36d75fa167"
 
     with client.chdir("build"):
-        client.run_command("cmake .. -DCMAKE_TOOLCHAIN_FILE=generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release")
+        client.run_command("cmake .. -DCMAKE_TOOLCHAIN_FILE=Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release")
         # Verify binary executable is found from build context package,
         # and library comes from host context package
         assert f"package/{build_context_package_id}/bin/foobin" in client.out
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py
index 30b5622b2..f108945ef 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py
@@ -38,7 +38,7 @@ def test_cmake_apple_bitcode_arc_and_visibility_flags_enabled(op_system, os_vers
     client.run("new hello/0.1 --template=cmake_lib")
     _add_message_status_flags(client)
     client.run("install . --profile:build=default --profile:host=host")
-    toolchain = client.load(os.path.join("build", "generators", "conan_toolchain.cmake"))
+    toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake"))
     # bitcode
     assert 'set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "YES")' in toolchain
     assert 'set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode")' in toolchain
@@ -121,7 +121,7 @@ def test_cmake_apple_bitcode_arc_and_visibility_flags_disabled(op_system, os_ver
     client.run("new hello/0.1 --template=cmake_lib")
     _add_message_status_flags(client)
     client.run("install . --profile:build=default --profile:host=host")
-    toolchain = client.load(os.path.join("build", "generators", "conan_toolchain.cmake"))
+    toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake"))
     # bitcode
     assert 'set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO")' in toolchain
     assert 'set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode")' not in toolchain
@@ -163,7 +163,7 @@ def test_cmake_apple_bitcode_arc_and_visibility_flags_are_none(op_system, os_ver
     client.run("new hello/0.1 --template=cmake_lib")
     _add_message_status_flags(client)
     client.run("install . --profile:build=default --profile:host=host")
-    toolchain = client.load(os.path.join("build", "generators", "conan_toolchain.cmake"))
+    toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake"))
     # bitcode
     assert 'set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO")' not in toolchain
     assert 'set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode")' not in toolchain
diff --git a/conans/test/integration/configuration/conf/test_conf_profile.py b/conans/test/integration/configuration/conf/test_conf_profile.py
index c2c208665..0f55ad7eb 100644
--- a/conans/test/integration/configuration/conf/test_conf_profile.py
+++ b/conans/test/integration/configuration/conf/test_conf_profile.py
@@ -164,3 +164,36 @@ def test_msbuild_compile_options():
     client.run("install . -pr=myprofile")
     msbuild_tool = client.load("conantoolchain_release_x64.props")
     assert "<ExceptionHandling>Async</ExceptionHandling>" in msbuild_tool
+
+
+def test_config_package_append(client):
+    profile1 = textwrap.dedent("""\
+        [conf]
+        user.myteam:myconf=["a", "b", "c"]
+        """)
+    profile2 = textwrap.dedent("""\
+        include(profile1)
+        [conf]
+        mypkg*:user.myteam:myconf+=["d"]
+        mydep*:user.myteam:myconf=+["e"]
+        """)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            def generate(self):
+                self.output.info(f"MYCONF: {self.conf.get('user.myteam:myconf')}")
+            def build(self):
+                self.output.info(f"MYCONFBUILD: {self.conf.get('user.myteam:myconf')}")
+            """)
+    client.save({"profile1": profile1,
+                 "profile2": profile2,
+                 "conanfile.py": conanfile})
+    client.run("install . mypkg/0.1@ -pr=profile2")
+    assert "conanfile.py (mypkg/0.1): MYCONF: ['a', 'b', 'c', 'd']" in client.out
+    client.run("install . mydep/0.1@ -pr=profile2")
+    assert "conanfile.py (mydep/0.1): MYCONF: ['e', 'a', 'b', 'c']" in client.out
+
+    client.run("create . mypkg/0.1@ -pr=profile2")
+    assert "mypkg/0.1: MYCONFBUILD: ['a', 'b', 'c', 'd']" in client.out
+    client.run("create . mydep/0.1@ -pr=profile2")
+    assert "mydep/0.1: MYCONFBUILD: ['e', 'a', 'b', 'c']" in client.out
diff --git a/conans/test/integration/graph/version_ranges/version_range_override_test.py b/conans/test/integration/graph/version_ranges/version_range_override_test.py
index c861a3992..0a23c78b3 100644
--- a/conans/test/integration/graph/version_ranges/version_range_override_test.py
+++ b/conans/test/integration/graph/version_ranges/version_range_override_test.py
@@ -90,3 +90,45 @@ class VersionRangeOverrideFailTestCase(unittest.TestCase):
         lock = t.load("conan.lock")
         self.assertIn("gtest/1.8.1@bloomberg/stable", lock)
         self.assertNotIn("gtest/1.8.0@PORT/stable", lock)
+
+    def test_override(self):
+        """
+        pkga -> ros_perception  -> ros_core
+           \\-----> pkgb  -----------/
+        """
+        # https://github.com/conan-io/conan/issues/8071
+        t = TestClient()
+        t.save({"conanfile.py": GenConanfile()})
+        t.run("create . ros_core/1.1.4@3rdparty/unstable")
+        t.run("create . ros_core/PR-53@3rdparty/snapshot")
+        t.save({"conanfile.py": GenConanfile().with_requires("ros_core/1.1.4@3rdparty/unstable")})
+        t.run("create . ros_perception/1.1.4@3rdparty/unstable")
+        t.run("create . ros_perception/PR-53@3rdparty/snapshot")
+        t.save({"conanfile.py": GenConanfile().with_requires("ros_core/[~=1.1]@3rdparty/unstable")})
+        t.run("create . pkgb/0.1@common/unstable")
+        t.save({"conanfile.py": GenConanfile("pkga", "0.1").with_requires(
+            "ros_perception/[~=1.1]@3rdparty/unstable",
+            "pkgb/[~=0]@common/unstable")})
+        t.run("create . ")
+        self.assertIn("ros_core/1.1.4@3rdparty/unstable", t.out)
+        self.assertIn("ros_perception/1.1.4@3rdparty/unstable", t.out)
+        self.assertNotIn("snapshot", t.out)
+
+        t.save({"conanfile.py": GenConanfile("pkga", "0.1")
+               .with_require("pkgb/[~=0]@common/unstable")
+               .with_require("ros_perception/PR-53@3rdparty/snapshot")
+               .with_require("ros_core/PR-53@3rdparty/snapshot", override=True)})
+
+        t.run("create .  --build=missing --build=pkga")
+        self.assertIn("ros_core/PR-53@3rdparty/snapshot", t.out)
+        self.assertIn("ros_perception/PR-53@3rdparty/snapshot", t.out)
+
+        # Override only the upstream without overriding the direct one
+        t.save({"conanfile.py": GenConanfile("pkga", "0.1")
+               .with_require("pkgb/[~=0]@common/unstable")
+               .with_require("ros_perception/[~=1.1]@3rdparty/unstable")
+               .with_require("ros_core/PR-53@3rdparty/snapshot", override=True)})
+
+        t.run("create .  --build=missing --build=pkga")
+        self.assertIn("ros_core/PR-53@3rdparty/snapshot", t.out)
+        self.assertIn("ros_perception/1.1.4@3rdparty/unstable", t.out)
diff --git a/conans/test/integration/layout/test_layout_paths.py b/conans/test/integration/layout/test_layout_paths.py
index 5d863f411..5049ac534 100644
--- a/conans/test/integration/layout/test_layout_paths.py
+++ b/conans/test/integration/layout/test_layout_paths.py
@@ -1,3 +1,4 @@
+import os
 import textwrap
 
 from conans.test.assets.genconanfile import GenConanfile
@@ -6,6 +7,7 @@ from conans.test.utils.tools import TestClient
 
 def test_editable_layout_paths():
     # https://github.com/conan-io/conan/issues/12521
+    # https://github.com/conan-io/conan/issues/12839
     c = TestClient()
     dep = textwrap.dedent("""
         import os
@@ -21,13 +23,16 @@ def test_editable_layout_paths():
             "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_settings("build_type", "arch")
                                                           .with_requires("dep/0.1")
                                                           .with_generator("CMakeDeps")
-                                                          .with_generator("PkgConfigDeps")})
+                                                          .with_generator("PkgConfigDeps")
+                                                          .with_generator("XcodeDeps")})
     c.run("editable add dep dep/0.1")
-    c.run("install pkg")
+    c.run("install pkg -s arch=x86_64")
     # It doesn't crash anymore
     assert "dep/0.1:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Editable" in c.out
-    arch = c.get_default_host_profile().settings['arch']
-    data = c.load(f"dep-release-{arch}-data.cmake")
+    data = c.load(f"dep-release-x86_64-data.cmake")
     assert 'set(dep_INCLUDE_DIRS_RELEASE "${dep_PACKAGE_FOLDER_RELEASE}/include")' in data
     pc = c.load("dep.pc")
     assert "includedir1=${prefix}/include" in pc
+    xcode = c.load("conan_dep_dep_release_x86_64.xcconfig")
+    dep_path = os.path.join(c.current_folder, "dep")
+    assert f"PACKAGE_ROOT_dep[config=Release][arch=x86_64][sdk=*] = {dep_path}" in xcode
diff --git a/conans/test/integration/py_requires/python_requires_test.py b/conans/test/integration/py_requires/python_requires_test.py
index c2d5bab40..174f92ad1 100644
--- a/conans/test/integration/py_requires/python_requires_test.py
+++ b/conans/test/integration/py_requires/python_requires_test.py
@@ -1020,3 +1020,103 @@ def test_multiple_reuse():
     c.run("install consumer")
     assert "VALUE COMMON: 42!!!" in c.out
     assert "VALUE TOOL: 42!!!" in c.out
+
+
+class TestTransitiveExtend:
+    # https://github.com/conan-io/conan/issues/10511
+    # https://github.com/conan-io/conan/issues/10565
+    def test_transitive_extend(self):
+        client = TestClient()
+        company = textwrap.dedent("""
+            from conan import ConanFile
+            class CompanyConanFile(ConanFile):
+                name = "company"
+                version = "1.0"
+
+                def msg1(self):
+                    return "company"
+                def msg2(self):
+                    return "company"
+            """)
+        project = textwrap.dedent("""
+            from conan import ConanFile
+            class ProjectBaseConanFile(ConanFile):
+                name = "project"
+                version = "1.0"
+
+                python_requires = "company/1.0"
+                python_requires_extend = "company.CompanyConanFile"
+
+                def msg1(self):
+                    return "project"
+            """)
+        consumer = textwrap.dedent("""
+            from conan import ConanFile
+            class Base(ConanFile):
+                name = "consumer"
+                version = "1.0"
+                python_requires = "project/1.0"
+                python_requires_extend = "project.ProjectBaseConanFile"
+                def generate(self):
+                    self.output.info("Msg1:{}!!!".format(self.msg1()))
+                    self.output.info("Msg2:{}!!!".format(self.msg2()))
+                """)
+        client.save({"company/conanfile.py": company,
+                     "project/conanfile.py": project,
+                     "consumer/conanfile.py": consumer})
+        client.run("export company")
+        client.run("export project")
+        client.run("install consumer")
+        assert "conanfile.py (consumer/1.0): Msg1:project!!!" in client.out
+        assert "conanfile.py (consumer/1.0): Msg2:company!!!" in client.out
+
+    def test_transitive_extend2(self):
+        client = TestClient()
+        company = textwrap.dedent("""
+            from conan import ConanFile
+            class CompanyConanFile(ConanFile):
+                name = "company"
+                version = "1.0"
+
+            class CompanyBase:
+                def msg1(self):
+                    return "company"
+                def msg2(self):
+                    return "company"
+            """)
+        project = textwrap.dedent("""
+            from conan import ConanFile
+            class ProjectBase:
+                def msg1(self):
+                    return "project"
+
+            class ProjectBaseConanFile(ConanFile):
+                name = "project"
+                version = "1.0"
+                python_requires = "company/1.0"
+
+                def init(self):
+                    pkg_name, base_class_name = "company", "CompanyBase"
+                    base_class = getattr(self.python_requires[pkg_name].module, base_class_name)
+                    global ProjectBase
+                    ProjectBase = type('ProjectBase', (ProjectBase, base_class, object), {})
+            """)
+        consumer = textwrap.dedent("""
+            from conan import ConanFile
+            class Base(ConanFile):
+                name = "consumer"
+                version = "1.0"
+                python_requires = "project/1.0"
+                python_requires_extend = "project.ProjectBase"
+                def generate(self):
+                    self.output.info("Msg1:{}!!!".format(self.msg1()))
+                    self.output.info("Msg2:{}!!!".format(self.msg2()))
+                """)
+        client.save({"company/conanfile.py": company,
+                     "project/conanfile.py": project,
+                     "consumer/conanfile.py": consumer})
+        client.run("export company")
+        client.run("export project")
+        client.run("install consumer")
+        assert "conanfile.py (consumer/1.0): Msg1:project!!!" in client.out
+        assert "conanfile.py (consumer/1.0): Msg2:company!!!" in client.out
diff --git a/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py b/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
index 6d3e9b57d..b3da541f9 100644
--- a/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
+++ b/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
@@ -1,4 +1,5 @@
 import os
+import platform
 import textwrap
 
 from conans.test.assets.genconanfile import GenConanfile
@@ -225,9 +226,14 @@ def test_dependency_props_from_consumer():
 
     client.save({"foo.py": foo.format(set_find_mode=""), "bar.py": bar}, clean_first=True)
 
-    module_file = os.path.join(client.current_folder, "build", "generators", "module-custom_bar_module_file_nameTargets.cmake")
-    components_module = os.path.join(client.current_folder, "build", "generators", "custom_bar_file_name-Target-release.cmake")
-    config_file = os.path.join(client.current_folder, "build", "generators", "custom_bar_file_nameTargets.cmake")
+    if platform.system() != "Windows":
+        gen_folder = os.path.join(client.current_folder, "build", "Release", "generators")
+    else:
+        gen_folder = os.path.join(client.current_folder, "build", "generators")
+
+    module_file = os.path.join(gen_folder, "module-custom_bar_module_file_nameTargets.cmake")
+    components_module = os.path.join(gen_folder, "custom_bar_file_name-Target-release.cmake")
+    config_file = os.path.join(gen_folder, "custom_bar_file_nameTargets.cmake")
 
     # uses cmake_find_mode set in bar: both
     client.run("create bar.py bar/1.0@")
@@ -311,19 +317,20 @@ def test_props_from_consumer_build_context_activated():
 
     client.save({"foo.py": foo.format(set_find_mode=""), "bar.py": bar}, clean_first=True)
 
-    module_file = os.path.join(client.current_folder, "build", "generators",
-                               "module-custom_bar_module_file_nameTargets.cmake")
-    components_module = os.path.join(client.current_folder, "build", "generators",
-                                     "custom_bar_file_name-Target-release.cmake")
-    config_file = os.path.join(client.current_folder, "build", "generators",
-                               "custom_bar_file_nameTargets.cmake")
+    if platform.system() != "Windows":
+        gen_folder = os.path.join(client.current_folder, "build", "Release", "generators")
+    else:
+        gen_folder = os.path.join(client.current_folder, "build", "generators")
+
+    module_file = os.path.join(gen_folder, "module-custom_bar_module_file_nameTargets.cmake")
+    components_module = os.path.join(gen_folder, "custom_bar_file_name-Target-release.cmake")
+    config_file = os.path.join(gen_folder, "custom_bar_file_nameTargets.cmake")
 
-    module_file_build = os.path.join(client.current_folder, "build", "generators",
+    module_file_build = os.path.join(gen_folder,
                                      "module-custom_bar_build_module_file_name_BUILDTargets.cmake")
-    components_module_build = os.path.join(client.current_folder, "build", "generators",
+    components_module_build = os.path.join(gen_folder,
                                            "custom_bar_build_file_name_BUILD-Target-release.cmake")
-    config_file_build = os.path.join(client.current_folder, "build", "generators",
-                                     "custom_bar_build_file_name_BUILDTargets.cmake")
+    config_file_build = os.path.join(gen_folder, "custom_bar_build_file_name_BUILDTargets.cmake")
 
     # uses cmake_find_mode set in bar: both
     client.run("create bar.py bar/1.0@ -pr:h=default -pr:b=default")
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index e219bcc5a..535179ba6 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -8,7 +8,6 @@ from mock import mock
 
 from conan.tools.cmake.presets import load_cmake_presets
 from conans.test.assets.genconanfile import GenConanfile
-from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import TestClient
 from conans.util.files import rmdir
 
@@ -379,7 +378,7 @@ def test_cmake_presets_binary_dir_available():
     else:
         build_dir = os.path.join(client.current_folder, "build")
 
-    presets = load_cmake_presets(os.path.join(client.current_folder, "build", "generators"))
+    presets = load_cmake_presets(os.path.join(build_dir, "generators"))
     assert presets["configurePresets"][0]["binaryDir"] == build_dir
 
 
@@ -402,12 +401,17 @@ def test_cmake_presets_multiconfig():
     presets = json.loads(client.load("CMakePresets.json"))
     assert len(presets["buildPresets"]) == 1
     assert presets["buildPresets"][0]["configuration"] == "Release"
+    assert len(presets["testPresets"]) == 1
+    assert presets["testPresets"][0]["configuration"] == "Release"
 
     client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile")
     presets = json.loads(client.load("CMakePresets.json"))
     assert len(presets["buildPresets"]) == 2
     assert presets["buildPresets"][0]["configuration"] == "Release"
     assert presets["buildPresets"][1]["configuration"] == "Debug"
+    assert len(presets["testPresets"]) == 2
+    assert presets["testPresets"][0]["configuration"] == "Release"
+    assert presets["testPresets"][1]["configuration"] == "Debug"
 
     client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=RelWithDebInfo "
                "--profile:h=profile")
@@ -418,16 +422,28 @@ def test_cmake_presets_multiconfig():
     assert presets["buildPresets"][1]["configuration"] == "Debug"
     assert presets["buildPresets"][2]["configuration"] == "RelWithDebInfo"
     assert presets["buildPresets"][3]["configuration"] == "MinSizeRel"
+    assert len(presets["testPresets"]) == 4
+    assert presets["testPresets"][0]["configuration"] == "Release"
+    assert presets["testPresets"][1]["configuration"] == "Debug"
+    assert presets["testPresets"][2]["configuration"] == "RelWithDebInfo"
+    assert presets["testPresets"][3]["configuration"] == "MinSizeRel"
 
     # Repeat one
     client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile")
     client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile")
+    presets = json.loads(client.load("CMakePresets.json"))
     assert len(presets["buildPresets"]) == 4
     assert presets["buildPresets"][0]["configuration"] == "Release"
     assert presets["buildPresets"][1]["configuration"] == "Debug"
     assert presets["buildPresets"][2]["configuration"] == "RelWithDebInfo"
     assert presets["buildPresets"][3]["configuration"] == "MinSizeRel"
 
+    assert len(presets["testPresets"]) == 4
+    assert presets["testPresets"][0]["configuration"] == "Release"
+    assert presets["testPresets"][1]["configuration"] == "Debug"
+    assert presets["testPresets"][2]["configuration"] == "RelWithDebInfo"
+    assert presets["testPresets"][3]["configuration"] == "MinSizeRel"
+
     assert len(presets["configurePresets"]) == 1
     assert presets["configurePresets"][0]["name"] == "default"
 
@@ -453,6 +469,9 @@ def test_cmake_presets_singleconfig():
     assert len(presets["buildPresets"]) == 1
     assert presets["buildPresets"][0]["configurePreset"] == "release"
 
+    assert len(presets["testPresets"]) == 1
+    assert presets["testPresets"][0]["configurePreset"] == "release"
+
     # Now two configurePreset, but named correctly
     client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile")
     presets = json.loads(client.load("CMakePresets.json"))
@@ -462,6 +481,9 @@ def test_cmake_presets_singleconfig():
     assert len(presets["buildPresets"]) == 2
     assert presets["buildPresets"][1]["configurePreset"] == "debug"
 
+    assert len(presets["testPresets"]) == 2
+    assert presets["testPresets"][1]["configurePreset"] == "debug"
+
     # Repeat configuration, it shouldn't add a new one
     client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile")
     presets = json.loads(client.load("CMakePresets.json"))
@@ -517,6 +539,7 @@ def test_toolchain_cache_variables():
     assert "-DCMAKE_TOOLCHAIN_FILE=" in client.out
     assert f"-G {_format_val('MinGW Makefiles')}" in client.out
 
+
 def test_android_c_library():
     client = TestClient()
     conanfile = textwrap.dedent("""
@@ -845,3 +868,36 @@ def test_set_cmake_lang_compilers_and_launchers():
     assert 'set(CMAKE_C_COMPILER "/my/local/gcc")' in toolchain
     assert 'set(CMAKE_CXX_COMPILER "g++")' in toolchain
     assert 'set(CMAKE_RC_COMPILER "C:/local/rc.exe")' in toolchain
+
+
+def test_cmake_layout_toolchain_folder():
+    """ in single-config generators, the toolchain is a different file per configuration
+    https://github.com/conan-io/conan/issues/12827
+    """
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout
+
+        class Conan(ConanFile):
+            settings = "os", "arch", "compiler", "build_type"
+            generators = "CMakeToolchain"
+
+            def layout(self):
+                cmake_layout(self)
+            """)
+    c.save({"conanfile.py": conanfile})
+    c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Release "
+          "-s compiler.libcxx=libstdc++11")
+    assert os.path.exists(os.path.join(c.current_folder,
+                                       "build/Release/generators/conan_toolchain.cmake"))
+    c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Debug "
+          "-s compiler.libcxx=libstdc++11")
+    assert os.path.exists(os.path.join(c.current_folder,
+                                       "build/Debug/generators/conan_toolchain.cmake"))
+    c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Debug "
+          "-s compiler.libcxx=libstdc++11 -s arch=x86 "
+          "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.arch\", \"settings.build_type\"]'")
+    print(c.out)
+    assert os.path.exists(os.path.join(c.current_folder,
+                                       "build/x86-debug/generators/conan_toolchain.cmake"))