diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 19a6db3dace..83967243151 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -63,6 +63,8 @@ def package_path(self, pref: PkgReference): app = ConanApp(self.conan_api) pref = _resolve_latest_pref(app, pref) ref_layout = app.cache.pkg_layout(pref) + if os.path.exists(ref_layout.finalize()): + return ref_layout.finalize() return _check_folder_existence(pref, "package", ref_layout.package()) def check_integrity(self, package_list): diff --git a/conan/internal/cache/conan_reference_layout.py b/conan/internal/cache/conan_reference_layout.py index 9933dcbbbb8..9bcd58160c7 100644 --- a/conan/internal/cache/conan_reference_layout.py +++ b/conan/internal/cache/conan_reference_layout.py @@ -10,6 +10,7 @@ SRC_FOLDER = "s" BUILD_FOLDER = "b" PACKAGES_FOLDER = "p" +FINALIZE_FOLDER = "f" EXPORT_FOLDER = "e" EXPORT_SRC_FOLDER = "es" DOWNLOAD_EXPORT_FOLDER = "d" @@ -55,7 +56,7 @@ def export_sources(self): return os.path.join(self._base_folder, EXPORT_SRC_FOLDER) def metadata(self): - return os.path.join(self.download_export(), "metadata") + return os.path.join(self.download_export(), METADATA) def download_export(self): return os.path.join(self._base_folder, DOWNLOAD_EXPORT_FOLDER) @@ -99,11 +100,14 @@ def build(self): def package(self): return os.path.join(self._base_folder, PACKAGES_FOLDER) + def finalize(self): + return os.path.join(self._base_folder, FINALIZE_FOLDER) + def download_package(self): return os.path.join(self._base_folder, DOWNLOAD_EXPORT_FOLDER) def metadata(self): - return os.path.join(self.download_package(), "metadata") + return os.path.join(self.download_package(), METADATA) def package_manifests(self): package_folder = self.package() diff --git a/conan/test/assets/genconanfile.py b/conan/test/assets/genconanfile.py index ebd4ae121a5..717b12b272a 100644 --- a/conan/test/assets/genconanfile.py +++ b/conan/test/assets/genconanfile.py @@ -27,6 +27,7 @@ def __init__(self, name=None, version=None): self._provides = None self._deprecated = None self._package_lines = None + self._finalize_lines = None self._package_files = None self._package_files_env = None self._package_files_link = None @@ -48,6 +49,8 @@ def __init__(self, name=None, version=None): self._cmake_build = False self._class_attributes = None + self._is_tested_ref_build_require = None + def with_package_type(self, value): self._package_type = value return self @@ -143,6 +146,10 @@ def with_test_requires(self, *refs): self._test_requires.append(ref_str) return self + def with_test_reference_as_build_require(self): + self._is_tested_ref_build_require = True + return self + def with_build_requirement(self, ref, **kwargs): self._build_requirements = self._build_requirements or [] ref_str = self._get_full_ref_str(ref) @@ -155,9 +162,10 @@ def with_tool_requirement(self, ref, **kwargs): self._tool_requirements.append((ref_str, kwargs)) return self - def with_import(self, i): - if i not in self._imports: - self._imports.append(i) + def with_import(self, *imports): + for i in imports: + if i not in self._imports: + self._imports.append(i) return self def with_setting(self, setting): @@ -207,6 +215,12 @@ def with_package(self, *lines): self._package_lines.append(line) return self + def with_finalize(self, *lines): + self._finalize_lines = self._finalize_lines or [] + for line in lines: + self._finalize_lines.append(line) + return self + def with_build_msg(self, msg): self._build_messages = self._build_messages or [] self._build_messages.append(msg) @@ -222,14 +236,16 @@ def with_package_info(self, cpp_info=None, env_info=None): self._package_info["env_info"] = env_info return self - def with_package_id(self, line): + def with_package_id(self, *lines): self._package_id_lines = self._package_id_lines or [] - self._package_id_lines.append(line) + for line in lines: + self._package_id_lines.append(line) return self - def with_test(self, line): + def with_test(self, *lines): self._test_lines = self._test_lines or [] - self._test_lines.append(line) + for line in lines: + self._test_lines.append(line) return self def with_cmake_build(self): @@ -360,6 +376,10 @@ def _package_method(self): return (self._package_lines or self._package_files or self._package_files_env or self._package_files_link) + @property + def _finalize_method(self): + return self._finalize_lines + @property def _package_method_render(self): lines = [] @@ -388,6 +408,19 @@ def package(self): {} """.format("\n".join(lines)) + @property + def _finalize_method_render(self): + lines = [] + if self._finalize_lines: + lines.extend(" {}".format(line) for line in self._finalize_lines) + + if not lines: + return "" + return """ + def finalize(self): +{} + """.format("\n".join(lines)) + @property def _build_render(self): if not self._build_messages and not self._cmake_build: @@ -432,11 +465,20 @@ def package_id(self): @property def _test_lines_render(self): - lines = ["", - " def requirements(self):", - ' self.requires(self.tested_reference_str)', - "", - ' def test(self):'] + [' %s' % m for m in self._test_lines] + if self._is_tested_ref_build_require: + lines = ["", + " def build_requirements(self):", + ' self.tool_requires(self.tested_reference_str)', + "", + ' def test(self):'] + else: + lines = ["", + " def requirements(self):", + ' self.requires(self.tested_reference_str)', + "", + ' def test(self):'] + + lines += [' %s' % m for m in self._test_lines] return "\n".join(lines) @property @@ -463,7 +505,8 @@ def __repr__(self): "exports_sources", "exports", "generators", "requires", "build_requires", "tool_requires", "test_requires", "requirements", "python_requires", "revision_mode", "settings", "options", "default_options", "build", - "package_method", "package_info", "package_id_lines", "test_lines" + "package_method", "package_info", "package_id_lines", "test_lines", + "finalize_method" ): if member == "requirements": # FIXME: This seems exclusive, but we could mix them? diff --git a/conans/client/installer.py b/conans/client/installer.py index ea679f44e56..b2937dda895 100644 --- a/conans/client/installer.py +++ b/conans/client/installer.py @@ -338,7 +338,9 @@ def _handle_package(self, package, install_reference, handled_count, total_count # Call the info method conanfile.folders.set_base_package(pkg_folder) conanfile.folders.set_base_pkg_metadata(pkg_metadata) - self._call_package_info(conanfile, pkg_folder, is_editable=False) + self._call_finalize_method(conanfile, package_layout.finalize()) + # Use package_folder which has been updated previously by install_method if necessary + self._call_package_info(conanfile, conanfile.package_folder, is_editable=False) def _handle_node_editable(self, install_node): # It will only run generation @@ -454,3 +456,15 @@ def _call_package_info(self, conanfile, package_folder, is_editable): self._hook_manager.execute("post_package_info", conanfile=conanfile) conanfile.cpp_info.check_component_requires(conanfile) + + def _call_finalize_method(self, conanfile, finalize_folder): + if hasattr(conanfile, "finalize"): + conanfile.folders.set_finalize_folder(finalize_folder) + if not os.path.exists(finalize_folder): + mkdir(finalize_folder) + conanfile.output.highlight("Calling finalize()") + with conanfile_exception_formatter(conanfile, "finalize"): + with conanfile_remove_attr(conanfile, ['cpp_info', 'settings', 'options'], 'finalize'): + conanfile.finalize() + + conanfile.output.success(f"Finalized folder {finalize_folder}") diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py index 0c6bedc7e6d..50cecb9b01e 100644 --- a/conans/model/conan_file.py +++ b/conans/model/conan_file.py @@ -164,6 +164,7 @@ def serialize(self): result["build_folder"] = self.build_folder result["generators_folder"] = self.generators_folder result["package_folder"] = self.package_folder + result["immutable_package_folder"] = self.immutable_package_folder result["cpp_info"] = self.cpp_info.serialize() result["conf_info"] = self.conf_info.serialize() @@ -318,6 +319,10 @@ def package_folder(self): """ return self.folders.base_package + @property + def immutable_package_folder(self): + return self.folders.immutable_package_folder + @property def generators_folder(self): return self.folders.generators_folder diff --git a/conans/model/conanfile_interface.py b/conans/model/conanfile_interface.py index 2130ff85be2..29a1607923f 100644 --- a/conans/model/conanfile_interface.py +++ b/conans/model/conanfile_interface.py @@ -39,6 +39,10 @@ def recipe_metadata_folder(self): def package_folder(self): return self._conanfile.package_folder + @property + def immutable_package_folder(self): + return self._conanfile.immutable_package_folder + @property def package_metadata_folder(self): return self._conanfile.package_metadata_folder diff --git a/conans/model/layout.py b/conans/model/layout.py index a06795550f8..de453f917fe 100644 --- a/conans/model/layout.py +++ b/conans/model/layout.py @@ -45,6 +45,7 @@ def __init__(self): self._base_recipe_metadata = None self._base_pkg_metadata = None + self._immutable_package_folder = None self.source = "" self.build = "" @@ -142,6 +143,14 @@ def package_folder(self): """For the cache, the package folder is only the base""" return self._base_package + def set_finalize_folder(self, folder): + self._immutable_package_folder = self.package_folder + self.set_base_package(folder) + + @property + def immutable_package_folder(self): + return self._immutable_package_folder or self.package_folder + @property def generators_folder(self): if self._base_generators is None: diff --git a/test/integration/conanfile/test_finalize_method.py b/test/integration/conanfile/test_finalize_method.py new file mode 100644 index 00000000000..0d1f02ce8bd --- /dev/null +++ b/test/integration/conanfile/test_finalize_method.py @@ -0,0 +1,363 @@ +import json +import os +import textwrap + +import pytest + +from conan.test.assets.genconanfile import GenConanfile + +from conan.test.utils.tools import TestClient +from conans.util.files import load, save + +conanfile_dep = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.files import save, copy + + class TestConan(ConanFile): + name = "dep" + version = "1.0" + def package(self): + save(self, os.path.join(self.package_folder, "file.txt"), "Hello World!") + save(self, os.path.join(self.package_folder, "file2.txt"), "Hello World 2!") + + def finalize(self): + self.output.info(f"Running finalize method in {self.package_folder}") + copy(self, "file.txt", src=self.immutable_package_folder, dst=self.package_folder) + save(self, os.path.join(self.package_folder, "finalized.txt"), "finalized file") + + def package_info(self): + self.output.info(f"Running package_info method in {self.package_folder}") + """) + + +class TestBasicLocalFlows: + + @pytest.fixture + def client(self): + tc = TestClient(light=True) + tc.save({"dep/conanfile.py": conanfile_dep}) + tc.run("export dep") + return tc + + def test_basic_finalize_method(self, client): + client.run("create dep") + layout = client.created_layout() + assert layout.package().endswith("p") + assert f"Package folder {layout.package()}" in client.out + assert f"Running finalize method in {layout.finalize()}" in client.out + assert f"Running package_info method in {layout.finalize()}" in client.out + client.run("install --requires=dep/1.0") + assert f"Running package_info method in {layout.finalize()}" in client.out + # Only issue is that the PackageLayout has no idea about the redirected package folder + # So we have to know to check for it in tests, but oh well + assert "finalized.txt" in os.listdir(layout.finalize()) + assert "finalized.txt" not in os.listdir(layout.package()) + + def test_dependency_finalize_method(self, client): + client.save({"app/conanfile.py": textwrap.dedent(""" + from conan import ConanFile + class TestConan(ConanFile): + name = "app" + version = "1.0" + requires = "dep/1.0" + def generate(self): + self.output.info("Running generate method") + dep_pkg_folder = self.dependencies["dep"].package_folder + self.output.info(f"Dep package folder: {dep_pkg_folder}") + """)}) + client.run("create dep") + dep_layout = client.created_layout() + client.run("create app") + assert f"Dep package folder: {dep_layout.package()}" not in client.out + assert f"Dep package folder: {dep_layout.finalize()}" in client.out + + def test_no_non_info_access(self): + client = TestClient(light=True) + client.save({"conanfile.py": GenConanfile("dep", "1.0") + .with_finalize("self.output.info('settings.os: ' + self.settings.os)")}) + client.run("create .", assert_error=True) + assert "'self.settings' access in 'finalize()' method is forbidden" in client.out + + def test_finalize_moves_from_package(self): + client = TestClient(light=True) + client.save({"conanfile.py": GenConanfile("dep", "1.0") + .with_import("from conan.tools.files import save, rename", + "import os") + .with_option("move", [True, False]) + .with_package('save(self, os.path.join(self.package_folder, "file.txt"), "Hello World!")', + "save(self, os.path.join(self.package_folder, 'file2.txt'), 'Hello World 2!')") + # This is NOT allowed, moving from package to finalize is forbidden, only as test to ensure consistency + .with_finalize("rename(self, os.path.join(self.immutable_package_folder, 'file.txt'), os.path.join(self.package_folder, 'file.txt')) if self.info.options.move else None")}) + client.run("create . -o=dep/*:move=True") + dep_moved_layout = client.created_layout() + assert "file.txt" in os.listdir(dep_moved_layout.finalize()) + assert "file.txt" not in os.listdir(dep_moved_layout.package()) + + client.run("create . -o=dep/*:move=False") + dep_kept_layout = client.created_layout() + assert "file.txt" not in os.listdir(dep_kept_layout.finalize()) + assert "file.txt" in os.listdir(dep_kept_layout.package()) + + # Now we can check that the package_id is the same for both + assert dep_moved_layout.reference.package_id != dep_kept_layout.reference.package_id + + # This now breaks if we try to cache check-integrity the moved package + client.run(f"cache check-integrity {dep_moved_layout.reference}", assert_error=True) + assert "There are corrupted artifacts" in client.out + + client.run(f"cache check-integrity {dep_kept_layout.reference}") + assert "There are corrupted artifacts" not in client.out + + def test_cache_path_command(self, client): + client.run("create dep") + dep_layout = client.created_layout() + pref = client.created_package_reference("dep/1.0") + client.run(f"cache path {pref}") + assert dep_layout.package() not in client.out + assert dep_layout.finalize() in client.out + + def test_remove_deletes_correct_folders(self, client): + client.run("create dep") + dep_layout = client.created_layout() + client.run("remove * -c") + assert not os.path.exists(dep_layout.package()) + assert not os.path.exists(dep_layout.finalize()) + + def test_save_restore_cache(self, client): + # Not created in the cache, just exported, nothing breaks because there is not even a package there + client.run("cache save *:*") + client.run("remove * -c") + client.run("cache restore conan_cache_save.tgz") + + # Now create the package and then save/restore + client.run("create dep") + dep_layout = client.created_layout() + client.run("cache save *:* -f=json", redirect_stdout="saved.json") + saved = json.loads(client.load("saved.json")) + pref = dep_layout.reference + saved_pkg_folder = saved["Local Cache"]["dep/1.0"]["revisions"][pref.ref.revision]["packages"][pref.package_id]["revisions"][pref.revision]["package_folder"] + assert saved_pkg_folder in dep_layout.package().replace("\\", "/") + client.run("remove * -c") + assert not os.path.exists(dep_layout.package()) + assert not os.path.exists(dep_layout.finalize()) + client.run("cache restore conan_cache_save.tgz") + client.run(f"cache path {dep_layout.reference}") + package_folder = client.out.strip() + + # The finalize() folder does not exist as restoring is not considered usage, so it never runs + # so this is just the immutable package_folder + assert "finalized.txt" not in os.listdir(package_folder) + + # But as soon as you call conan finalize, finalize() is called and so it's used, + # so package_folder will be the finalize folder + client.run("install --requires=dep/1.0") + client.run(f"cache path {dep_layout.reference}") + package_folder = client.out.strip() + assert "finalized.txt" in os.listdir(package_folder) + + def test_graph_info_output(self, client): + client.run("create dep") + dep_layout = client.created_layout() + client.run("install --requires=dep/1.0 -f=json", redirect_stdout="finalize.json") + finalize_output = json.loads(client.load("finalize.json")) + assert finalize_output["graph"]["nodes"]["1"]["package_folder"] == dep_layout.finalize() + assert finalize_output["graph"]["nodes"]["1"]["immutable_package_folder"] == dep_layout.package() + + def test_create_pkglist_output(self, client): + client.run("create dep -f=json", redirect_stdout="created.json") + created_pkgid = client.created_package_id("dep/1.0") + client.run("list --graph=created.json --graph-binaries=build") + assert created_pkgid in client.out + + def test_vendorized_basic(self, client): + client.run("create dep") + client.save({"vendor/conanfile.py": GenConanfile("vendor", "1.0") + .with_import("from conan.tools.files import copy") + .with_class_attribute("vendor=True") + .with_requires("dep/1.0") + .with_package("copy(self, 'file.txt', src=self.dependencies['dep'].package_folder, dst=self.package_folder)", + "copy(self, 'finalized.txt', src=self.dependencies['dep'].package_folder, dst=self.package_folder)", + "copy(self, 'file2.txt', src=self.dependencies['dep'].immutable_package_folder, dst=self.package_folder)")}) + client.run("create vendor") + vendor_layout = client.created_layout() + assert "file.txt" in os.listdir(vendor_layout.package()) + assert "finalized.txt" in os.listdir(vendor_layout.package()) + assert "file2.txt" in os.listdir(vendor_layout.package()) + + def test_check_integrity(self, client): + client.run("create dep") + dep_layout = client.created_layout() + client.run(f"cache check-integrity {dep_layout.reference}") + assert "There are corrupted artifacts" not in client.out + # Even if we re-change the finalize folder contents, it should still be fine + save(os.path.join(dep_layout.finalize(), "finalized.txt"), "Modified!") + client.run(f"cache check-integrity {dep_layout.reference}") + assert "There are corrupted artifacts" not in client.out + # But as soon as we change the package, it should still fail like a normal package would + save(os.path.join(dep_layout.package(), "file.txt"), "Modified!") + client.run(f"cache check-integrity {dep_layout.reference}", assert_error=True) + assert "There are corrupted artifacts" in client.out + + @pytest.mark.parametrize("with_finalize_method", [True, False]) + def test_access_immutable_from_consumer(self, client, with_finalize_method): + if not with_finalize_method: + client.save({"dep/conanfile.py": GenConanfile("dep", "1.0")}) + client.save({"app/conanfile.py": GenConanfile("app", "1.0") + .with_requires("dep/1.0") + .with_package("dep = self.dependencies['dep/1.0']", + "self.output.info(f'Immutable package: {dep.immutable_package_folder}')", + # TODO: Think about if we want this interface + # "self.output.info(f'finalize: {dep.finalize_folder}')", + "self.output.info(f'Package: {dep.package_folder}')")}) + client.run("create dep") + dep_layout = client.created_layout() + client.run("create app") + assert f"app/1.0: Immutable package: {dep_layout.package()}" in client.out + # assert f"app/1.0: finalize: {dep_layout.finalize()}" in client.out + if with_finalize_method: + assert f"app/1.0: Package: {dep_layout.finalize()}" in client.out + else: + assert f"app/1.0: Package: {dep_layout.package()}" in client.out + + def test_cache_modification_of_custom_conf_based_on_settings(self): + tc = TestClient(light=True) + tc.save({"conanfile.py": GenConanfile("dep", "1.0") + .with_import("from conan.tools.files import save", + "import os") + .with_option("myoption", [True, False]) + .with_option("otheroption", [True, False]) + .with_default_option("myoption", False) + .with_default_option("otheroption", False) + .with_setting("os") + .with_package_id("del self.info.options.myoption") + .with_finalize("save(self, os.path.join(self.package_folder, 'file.txt'), 'Hello World!')", + "save(self, os.path.join(self.package_folder, 'os.conf'), str(self.info.settings.os))", + "save(self, os.path.join(self.package_folder, 'option.conf'), str(self.info.options.get_safe('myoption')))", + "save(self, os.path.join(self.package_folder, 'otheroption.conf'), str(self.info.options.otheroption))")}) + tc.run("create . -s=os=Linux -o=&:myoption=True -o=&:otheroption=True") + layout = tc.created_layout() + assert "file.txt" in os.listdir(layout.finalize()) + assert tc.load(os.path.join(layout.finalize(), "os.conf")) == "Linux" + # This is problematic, it means that the mapping for finalize() and package_id would not be 1:1 and could be outdated + assert tc.load(os.path.join(layout.finalize(), "option.conf")) == "None" + assert tc.load(os.path.join(layout.finalize(), "otheroption.conf")) == "True" + + +class TestToolRequiresFlows: + def test_tool_requires(self): + tc = TestClient(light=True) + tc.save({"dep/conanfile.py": textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.files import save, copy + + class TestConan(ConanFile): + name = "dep" + version = "1.0" + package_type = "application" + def package(self): + save(self, os.path.join(self.package_folder, "bin", "executable.txt"), "Base") + + def finalize(self): + self.output.info(f"Running finalize method in {self.package_folder}") + copy(self, "*", src=self.immutable_package_folder, dst=self.package_folder) + save(self, os.path.join(self.package_folder, "bin", "finalized.txt"), "finalized file") + + def package_info(self): + self.output.info(f"Running package_info method in {self.package_folder}") + self.cpp_info.bindirs = ["bin"] + + """), "app/conanfile.py": textwrap.dedent(""" + from conan import ConanFile + import os + + class TestConan(ConanFile): + name = "app" + version = "1.0" + + def build_requirements(self): + self.tool_requires("dep/1.0") + + def build(self): + self.output.info("Running build method") + bindir = self.dependencies.build['dep'].cpp_info.bindir + self.output.info(f"Dep bindir: {bindir}") + self.output.info(f"Is finalized? {os.path.exists(os.path.join(bindir, 'finalized.txt'))}") + """)}) + tc.run("create dep --build-require") + dep_layout = tc.created_layout() + tc.run("create app") + # This fails. cpp_info is using the original package folder to construct the final path + assert f"Dep bindir: {dep_layout.finalize()}" in tc.out + assert "app/1.0: Is finalized? True" in tc.out + + def test_test_package_uses_created_tool_which_modifies_pkgfolder(self): + tc = TestClient(light=True) + tc.save({"conanfile.py": GenConanfile("app", "1.0") + .with_import("from conan.tools.files import save") + .with_package_type("application") + .with_package("save(self, 'file.txt', 'Hello World!')") + .with_package_info({"bindirs": ["bin"]}, {}) + .with_finalize("save(self, 'finalized.txt', 'finalized file')"), + "test_package/conanfile.py": GenConanfile() + .with_import("from conan.tools.files import save", + "import os") + .with_test_reference_as_build_require() + .with_test("bindir = self.dependencies.build[self.tested_reference_str].cpp_info.bindir", + "self.output.info(f'Bindir: {bindir}')", + "save(self, os.path.join(bindir, '__pycache__.pyc'), 'Test file')")}) + tc.run("create . --build-require") + app_layout = tc.created_layout() + assert f"Bindir: {os.path.join(app_layout.finalize(), 'bin')}" in tc.out + tc.run(f"cache check-integrity {app_layout.reference}") + assert "There are corrupted artifacts" not in tc.out + + +class TestRemoteFlows: + + @pytest.fixture + def client(self): + tc = TestClient(light=True, default_server_user=True) + tc.save({"dep/conanfile.py": conanfile_dep}) + tc.run("export dep") + return tc + + def test_remote_upload_finalize_method(self, client): + client.run("create dep") + created_pref = client.created_package_reference("dep/1.0") + client.run("upload * -r=default -c") + + # Only the package folder is uploaded, not the finalize folder + uploaded_pref_path = client.servers["default"].test_server.server_store.package(created_pref) + manifest_contents = load(os.path.join(uploaded_pref_path, "conanmanifest.txt")) + assert "file.txt" in manifest_contents + assert "finalized.txt" not in manifest_contents + + client.run("remove * -c") + client.run(f"download {created_pref} -r=default") + downloaded_pref_layout = client.get_latest_pkg_layout(created_pref) + assert "file.txt" in os.listdir(downloaded_pref_layout.package()) + # Download is not an "usage" of the package, so no finalize() is yet executed + assert "finalized.txt" not in os.listdir(downloaded_pref_layout.package()) + assert not os.path.exists(os.path.join(downloaded_pref_layout.finalize())) + + client.run(f"cache path {created_pref}") + package_folder = client.out.strip() + assert package_folder == downloaded_pref_layout.package() + assert package_folder.endswith("p") + # Now this finalize will run the finalize() method + client.run("install --requires=dep/1.0") + assert f"Running finalize method in {downloaded_pref_layout.finalize()}" in client.out + + client.run("remove * -c") + client.run("install --requires=dep/1.0 -r=default") + assert "dep/1.0: Calling finalize()" + assert f"Running finalize method in {downloaded_pref_layout.finalize()}" in client.out + + def test_upload_verify_integrity(self, client): + client.run("create dep") + dep_layout = client.created_layout() + client.run("upload * -r=default -c --check") + assert f"dep/1.0:{dep_layout.reference.package_id}: Integrity checked: ok" in client.out + assert "There are corrupted artifacts" not in client.out