def test_build_generics_symlink_dir(tmp_path): """Respects a symlinked dir.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() somedir = tmp_path / "somedir" somedir.mkdir() somefile = somedir / "sanity check" somefile.touch() the_symlink = tmp_path / "thelink" the_symlink.symlink_to(somedir) builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=entrypoint, ) builder.handle_generic_paths() built_symlink = build_dir / "thelink" assert built_symlink.is_symlink() assert built_symlink.resolve() == build_dir / "somedir" real_link = os.readlink(str(built_symlink)) assert real_link == "somedir" # as a sanity check, the file inside the linked dir should exist assert (build_dir / "thelink" / "sanity check").exists()
def test_build_generics_ignored_file(tmp_path, emitter): """Don't include ignored filed.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") # create two files (and the needed entrypoint) file1 = tmp_path / "file1.txt" file1.touch() file2 = tmp_path / "file2.txt" file2.touch() entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=entrypoint, ) # set it up to ignore file 2 and make it work builder.ignore_rules.extend_patterns(["file2.*"]) builder.handle_generic_paths() assert (build_dir / "file1.txt").exists() assert not (build_dir / "file2.txt").exists() expected = "Ignoring file because of rules: 'file2.txt'" emitter.assert_trace(expected)
def test_build_dependencies_virtualenv_multiple(tmp_path): """A virtualenv is created with multiple requirements files.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), requirements=["reqs1.txt", "reqs2.txt"], ) with patch("charmcraft.charm_builder.subprocess.run") as mock_run: mock_run.return_value.returncode = 1 with patch("charmcraft.charm_builder._process_run") as mock: mock.return_value = 0 builder.handle_dependencies() envpath = build_dir / VENV_DIRNAME assert mock.mock_calls == [ call(["pip3", "--version"]), call([ "pip3", "install", "--target={}".format(envpath), "--requirement=reqs1.txt", "--requirement=reqs2.txt", ]), ]
def test_build_generics_symlink_deep(tmp_path): """Correctly re-links a symlink across deep dirs.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() dir1 = tmp_path / "dir1" dir1.mkdir() dir2 = tmp_path / "dir2" dir2.mkdir() original_target = dir1 / "file.real" original_target.touch() the_symlink = dir2 / "file.link" the_symlink.symlink_to(original_target) builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=entrypoint, ) builder.handle_generic_paths() built_symlink = build_dir / "dir2" / "file.link" assert built_symlink.is_symlink() assert built_symlink.resolve() == build_dir / "dir1" / "file.real" real_link = os.readlink(str(built_symlink)) assert real_link == "../dir1/file.real"
def test_build_generics_ignored_dir(tmp_path, caplog): """Don't include ignored dir.""" caplog.set_level(logging.DEBUG) build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") # create two files (and the needed entrypoint) dir1 = tmp_path / "dir1" dir1.mkdir() dir2 = tmp_path / "dir2" dir2.mkdir() entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=entrypoint, ) # set it up to ignore dir 2 and make it work builder.ignore_rules.extend_patterns(["dir2"]) builder.handle_generic_paths() assert (build_dir / "dir1").exists() assert not (build_dir / "dir2").exists() expected = "Ignoring directory because of rules: 'dir2'" assert expected in [rec.message for rec in caplog.records]
def test_build_dependencies_needs_system(tmp_path, config): """pip3 is called with --system when pip3 needs it.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), requirements=["reqs"], ) with patch("charmcraft.charm_builder.subprocess.run") as mock_run: mock_run.return_value.returncode = 0 with patch("charmcraft.charm_builder._process_run") as mock: mock.return_value = 0 builder.handle_dependencies() envpath = build_dir / VENV_DIRNAME assert mock.mock_calls == [ call(["pip3", "--version"]), call([ "pip3", "install", "--target={}".format(envpath), "--system", "--requirement=reqs", ]), ]
def test_build_generics_different_filetype(tmp_path, emitter, monkeypatch): """Ignores whatever is not a regular file, symlink or dir.""" # change into the tmp path and do everything locally, because otherwise the socket path # will be too long for mac os monkeypatch.chdir(tmp_path) metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = pathlib.Path(BUILD_DIRNAME) build_dir.mkdir() entrypoint = pathlib.Path("crazycharm.py") entrypoint.touch() # create a socket sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind("test-socket") builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=tmp_path / entrypoint, ) builder.handle_generic_paths() assert not (build_dir / "test-socket").exists() expected = "Ignoring file because of type: 'test-socket'" emitter.assert_trace(expected)
def test_build_generics_symlink_directory_outside(tmp_path, emitter): """Ignores (with warning) a symlink pointing a dir outside projects dir.""" project_dir = tmp_path / "test-project" project_dir.mkdir() metadata = project_dir / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = project_dir / BUILD_DIRNAME build_dir.mkdir() entrypoint = project_dir / "crazycharm.py" entrypoint.touch() outside_project = tmp_path / "dangerous" outside_project.mkdir() the_symlink = project_dir / "external-dir" the_symlink.symlink_to(outside_project) builder = CharmBuilder( charmdir=project_dir, builddir=build_dir, entrypoint=entrypoint, ) builder.handle_generic_paths() assert not (build_dir / "external-dir").exists() expected = "Ignoring symlink because targets outside the project: 'external-dir'" emitter.assert_trace(expected)
def test_build_dispatcher_classic_hooks_linking_charm_replaced(tmp_path, emitter): """Hooks that are just a symlink to the entrypoint are replaced.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() # simple source code src_dir = build_dir / "src" src_dir.mkdir() built_charm_script = src_dir / "charm.py" with built_charm_script.open("wb") as fh: fh.write(b"all the magic") # a test hook, just a symlink to the charm built_hooks_dir = build_dir / "hooks" built_hooks_dir.mkdir() test_hook = built_hooks_dir / "somehook" test_hook.symlink_to(built_charm_script) included_dispatcher = build_dir / DISPATCH_FILENAME builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), ) builder.handle_dispatcher(built_charm_script) # the test hook is still there and a symlink, but now pointing to the dispatcher assert test_hook.is_symlink() assert test_hook.resolve() == included_dispatcher expected = "Replacing existing hook 'somehook' as it's a symlink to the entrypoint" emitter.assert_trace(expected)
def test_build_dispatcher_classic_hooks_mandatory_respected(tmp_path): """The already included mandatory classic hooks are left untouched.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() built_hooks_dir = build_dir / "hooks" built_hooks_dir.mkdir() test_hook = built_hooks_dir / "testhook" with test_hook.open("wb") as fh: fh.write(b"abc") linked_entrypoint = build_dir / "somestuff.py" builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), ) with patch("charmcraft.charm_builder.MANDATORY_HOOK_NAMES", {"testhook"}): builder.handle_dispatcher(linked_entrypoint) with test_hook.open("rb") as fh: assert fh.read() == b"abc"
def test_build_dependencies_virtualenv_binary_packages(tmp_path): """A virtualenv is created with the specified packages.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), binary_python_packages=["pkg1", "pkg2"], python_packages=[], requirements=[], ) with patch("charmcraft.charm_builder._process_run") as mock: with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() pip_cmd = str(charm_builder._find_venv_bin(tmp_path / STAGING_VENV_DIRNAME, "pip3")) assert mock.mock_calls == [ call(["python3", "-m", "venv", str(tmp_path / STAGING_VENV_DIRNAME)]), call([pip_cmd, "--version"]), call([pip_cmd, "install", "--upgrade", "pkg1", "pkg2"]), ] site_packages_dir = charm_builder._find_venv_site_packages(pathlib.Path(STAGING_VENV_DIRNAME)) assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / VENV_DIRNAME)]
def test_build_generics_symlink_file_outside(tmp_path, caplog): """Ignores (with warning) a symlink pointing a file outside projects dir.""" caplog.set_level(logging.WARNING) project_dir = tmp_path / "test-project" project_dir.mkdir() metadata = project_dir / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = project_dir / BUILD_DIRNAME build_dir.mkdir() entrypoint = project_dir / "crazycharm.py" entrypoint.touch() outside_project = tmp_path / "dangerous.txt" outside_project.touch() the_symlink = project_dir / "external-file" the_symlink.symlink_to(outside_project) builder = CharmBuilder( charmdir=project_dir, builddir=build_dir, entrypoint=entrypoint, ) builder.handle_generic_paths() assert not (build_dir / "external-file").exists() expected = "Ignoring symlink because targets outside the project: 'external-file'" assert expected in [rec.message for rec in caplog.records]
def test_build_generics_simple_files(tmp_path): """Check transferred metadata and simple entrypoint, also return proper linked entrypoint.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=entrypoint, ) linked_entrypoint = builder.handle_generic_paths() # check files are there, are files, and are really hard links (so no # check for permissions needed) built_metadata = build_dir / CHARM_METADATA assert built_metadata.is_file() assert built_metadata.stat().st_ino == metadata.stat().st_ino built_entrypoint = build_dir / "crazycharm.py" assert built_entrypoint.is_file() assert built_entrypoint.stat().st_ino == entrypoint.stat().st_ino assert linked_entrypoint == built_entrypoint
def test_build_dependencies_virtualenv_all(tmp_path, emitter): """A virtualenv is created with the specified packages.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() reqs_file_1 = tmp_path / "reqs.txt" reqs_file_1.touch() reqs_file_2 = tmp_path / "reqs.txt" reqs_file_1.touch() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), binary_python_packages=["pkg1", "pkg2"], python_packages=["pkg3", "pkg4"], requirements=[reqs_file_1, reqs_file_2], ) with patch("charmcraft.charm_builder._process_run") as mock: with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() pip_cmd = str( charm_builder._find_venv_bin(tmp_path / STAGING_VENV_DIRNAME, "pip3")) assert mock.mock_calls == [ call(["python3", "-m", "venv", str(tmp_path / STAGING_VENV_DIRNAME)]), call([pip_cmd, "--version"]), call([pip_cmd, "install", "--upgrade", "pkg1", "pkg2"]), call([ pip_cmd, "install", "--upgrade", "--no-binary", ":all:", "pkg3", "pkg4" ]), call([ pip_cmd, "install", "--upgrade", "--no-binary", ":all:", f"--requirement={reqs_file_1}", f"--requirement={reqs_file_2}", ]), ] site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(STAGING_VENV_DIRNAME)) assert mock_copytree.mock_calls == [ call(site_packages_dir, build_dir / VENV_DIRNAME) ] emitter.assert_trace("Handling dependencies") emitter.assert_progress("Installing dependencies")
def test_builder_without_jujuignore(tmp_path): """Without a .jujuignore we still have a default set of ignores""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), ) ignore = builder._load_juju_ignore() assert ignore.match("/.git", is_dir=True) assert ignore.match("/build", is_dir=True) assert not ignore.match("myfile.py", is_dir=False)
def test_build_dependencies_virtualenv_none(tmp_path): """The virtualenv is NOT created if no needed.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), requirements=[], ) with patch("charmcraft.charm_builder.subprocess.run") as mock_run: builder.handle_dependencies() mock_run.assert_not_called()
def test_build_dependencies_virtualenv_error_basicpip(tmp_path): """Process is properly interrupted if using pip fails.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), requirements=["something"], ) with patch("charmcraft.charm_builder._process_run") as mock: mock.return_value = -7 with pytest.raises(CommandError, match="problems using pip"): builder.handle_dependencies()
def test_build_dependencies_virtualenv_error_installing(tmp_path): """Process is properly interrupted if virtualenv creation fails.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), requirements=["something"], ) with patch("charmcraft.charm_builder._process_run") as mock: mock.side_effect = [0, -7] with pytest.raises(CommandError, match="problems installing dependencies"): builder.handle_dependencies()
def test_build_dependencies_no_reused_missing_hash_file(tmp_path, emitter): """Dependencies are built again because previous hash file was not found.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), binary_python_packages=[], python_packages=["ops"], requirements=[], ) staging_venv_dir = tmp_path / STAGING_VENV_DIRNAME # patch the dependencies installation method so it skips all subprocessing but actually # creates the directory, to simplify testing builder._install_dependencies = lambda dirpath: dirpath.mkdir(exist_ok=True ) # first run! with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() emitter.assert_trace("Handling dependencies") emitter.assert_trace("Dependencies directory not found") emitter.assert_progress("Installing dependencies") # directory created and packages installed assert staging_venv_dir.exists() # installation directory copied to the build directory site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(STAGING_VENV_DIRNAME)) assert mock_copytree.mock_calls == [ call(site_packages_dir, build_dir / VENV_DIRNAME) ] # remove the hash file (tmp_path / DEPENDENCIES_HASH_FILENAME).unlink() # second run! emitter.interactions.clear() with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() emitter.assert_trace("Handling dependencies") emitter.assert_trace("Dependencies hash file not found") emitter.assert_progress("Installing dependencies") # directory created and packages installed *again* assert staging_venv_dir.exists() # installation directory copied *again* to the build directory site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(STAGING_VENV_DIRNAME)) assert mock_copytree.mock_calls == [ call(site_packages_dir, build_dir / VENV_DIRNAME) ]
def test_build_dependencies_virtualenv_none(tmp_path, emitter): """The virtualenv is NOT created if no needed.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), binary_python_packages=[], python_packages=[], requirements=[], ) with patch("charmcraft.charm_builder.subprocess.run") as mock_run: builder.handle_dependencies() mock_run.assert_not_called() emitter.assert_trace("Handling dependencies") emitter.assert_trace("No dependencies to handle")
def test_build_dispatcher_modern_dispatch_respected(tmp_path): """The already included dispatcher script is left untouched.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() already_present_dispatch = build_dir / DISPATCH_FILENAME with already_present_dispatch.open("wb") as fh: fh.write(b"abc") builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), ) builder.handle_dispatcher("whatever") with already_present_dispatch.open("rb") as fh: assert fh.read() == b"abc"
def test_build_dispatcher_modern_dispatch_created(tmp_path): """The dispatcher script is properly built.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() linked_entrypoint = build_dir / "somestuff.py" builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), ) builder.handle_dispatcher(linked_entrypoint) included_dispatcher = build_dir / DISPATCH_FILENAME with included_dispatcher.open("rt", encoding="utf8") as fh: dispatcher_code = fh.read() assert dispatcher_code == DISPATCH_CONTENT.format(entrypoint_relative_path="somestuff.py")
def test_build_dependencies_virtualenv_simple(tmp_path): """A virtualenv is created with the specified requirements file.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), requirements=["reqs.txt"], ) with patch("charmcraft.charm_builder.subprocess.run") as mock_run: mock_run.return_value.returncode = 1 with patch("charmcraft.charm_builder._process_run") as mock: mock.return_value = 0 builder.handle_dependencies() envpath = build_dir / VENV_DIRNAME assert mock.mock_calls == [ call(["pip3", "--version"]), call([ "pip3", "install", "--target={}".format(envpath), "--requirement=reqs.txt" ]), ] assert mock_run.mock_calls == [ call( [ "python3", "-c", ("from pip.commands.install import InstallCommand; " 'assert InstallCommand().cmd_opts.get_option("--system") is not None' ), ], stdout=-3, stderr=-3, ), ]
def test_build_dependencies_reused(tmp_path, emitter): """Happy case to reuse dependencies from last run.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() reqs_file = tmp_path / "reqs.txt" reqs_file.touch() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), binary_python_packages=[], python_packages=[], requirements=[reqs_file], ) staging_venv_dir = tmp_path / STAGING_VENV_DIRNAME # patch the dependencies installation method so it skips all subprocessing but actually # creates the directory, to simplify testing; note that we specifically are calling mkdir # to fail if the directory is already there, so we ensure it is called once builder._install_dependencies = lambda dirpath: dirpath.mkdir(exist_ok= False) # first run! with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() emitter.assert_trace("Handling dependencies") emitter.assert_trace("Dependencies directory not found") emitter.assert_progress("Installing dependencies") # directory created and packages installed assert staging_venv_dir.exists() # installation directory copied to the build directory site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(STAGING_VENV_DIRNAME)) assert mock_copytree.mock_calls == [ call(site_packages_dir, build_dir / VENV_DIRNAME) ] # second run! emitter.interactions.clear() with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() emitter.assert_trace("Handling dependencies") emitter.assert_trace( "Reusing installed dependencies, they are equal to last run ones") # installation directory copied *again* to the build directory (this is always done as # buildpath is cleaned) site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(STAGING_VENV_DIRNAME)) assert mock_copytree.mock_calls == [ call(site_packages_dir, build_dir / VENV_DIRNAME) ]
def test_builder_with_jujuignore(tmp_path): """With a .jujuignore we will include additional ignores.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() with (tmp_path / ".jujuignore").open("w", encoding="utf-8") as ignores: ignores.write("*.py\n" "/h\xef.txt\n") builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), ) ignore = builder._load_juju_ignore() assert ignore.match("/.git", is_dir=True) assert ignore.match("/build", is_dir=True) assert ignore.match("myfile.py", is_dir=False) assert not ignore.match("hi.txt", is_dir=False) assert ignore.match("h\xef.txt", is_dir=False) assert not ignore.match("myfile.c", is_dir=False)
def test_build_generics_simple_dir(tmp_path): """Check transferred any directory, with proper permissions.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") somedir = tmp_path / "somedir" somedir.mkdir(mode=0o700) builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=entrypoint, ) builder.handle_generic_paths() built_dir = build_dir / "somedir" assert built_dir.is_dir() assert built_dir.stat().st_mode & 0xFFF == 0o700
def test_build_dispatcher_classic_hooks_mandatory_created(tmp_path): """The mandatory classic hooks are implemented ok if not present.""" metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() linked_entrypoint = build_dir / "somestuff.py" included_dispatcher = build_dir / DISPATCH_FILENAME builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), ) with patch("charmcraft.charm_builder.MANDATORY_HOOK_NAMES", {"testhook"}): builder.handle_dispatcher(linked_entrypoint) test_hook = build_dir / "hooks" / "testhook" assert test_hook.is_symlink() assert test_hook.resolve() == included_dispatcher real_link = os.readlink(str(test_hook)) assert real_link == os.path.join("..", DISPATCH_FILENAME)
def test_build_generics_symlink_file(tmp_path): """Respects a symlinked file.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() the_symlink = tmp_path / "somehook.py" the_symlink.symlink_to(entrypoint) builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=entrypoint, ) builder.handle_generic_paths() built_symlink = build_dir / "somehook.py" assert built_symlink.is_symlink() assert built_symlink.resolve() == build_dir / "crazycharm.py" real_link = os.readlink(str(built_symlink)) assert real_link == "crazycharm.py"
def test_build_dependencies_no_reused_problematic_hash_file(tmp_path, emitter): """Dependencies are built again because having problems to read the previous hash file.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=pathlib.Path("whatever"), binary_python_packages=[], python_packages=["ops"], requirements=[], ) staging_venv_dir = tmp_path / STAGING_VENV_DIRNAME # patch the dependencies installation method so it skips all subprocessing but actually # creates the directory, to simplify testing builder._install_dependencies = lambda dirpath: dirpath.mkdir(exist_ok=True ) # first run! with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() emitter.assert_trace("Handling dependencies") emitter.assert_trace("Dependencies directory not found") emitter.assert_progress("Installing dependencies") # directory created and packages installed assert staging_venv_dir.exists() # installation directory copied to the build directory site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(STAGING_VENV_DIRNAME)) assert mock_copytree.mock_calls == [ call(site_packages_dir, build_dir / VENV_DIRNAME) ] # avoid the file to be read succesfully (tmp_path / DEPENDENCIES_HASH_FILENAME).write_bytes( b"\xc3\x28") # invalid UTF8 # second run! emitter.interactions.clear() with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() emitter.assert_trace("Handling dependencies") emitter.assert_trace( "Problems reading the dependencies hash file: " "'utf-8' codec can't decode byte 0xc3 in position 0: invalid continuation byte" ) emitter.assert_progress("Installing dependencies") # directory created and packages installed *again* assert staging_venv_dir.exists() # installation directory copied *again* to the build directory site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(STAGING_VENV_DIRNAME)) assert mock_copytree.mock_calls == [ call(site_packages_dir, build_dir / VENV_DIRNAME) ]
def _test_build_generics_tree(tmp_path, *, expect_hardlinks): build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() # create this structure: # ├─ crazycharm.py (entrypoint) # ├─ file1.txt # ├─ dir1 # │ └─ dir3 (ignored!) # └─ dir2 # ├─ file2.txt # ├─ file3.txt (ignored!) # ├─ dir4 (ignored!) # │ └─ file4.txt # └─ dir5 entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() metadata = tmp_path / CHARM_METADATA metadata.write_text("name: crazycharm") file1 = tmp_path / "file1.txt" file1.touch() dir1 = tmp_path / "dir1" dir1.mkdir() dir3 = dir1 / "dir3" dir3.mkdir() dir2 = tmp_path / "dir2" dir2.mkdir() file2 = dir2 / "file2.txt" file2.touch() file3 = dir2 / "file3.txt" file3.touch() dir4 = dir2 / "dir4" dir4.mkdir() file4 = dir4 / "file4.txt" file4.touch() dir5 = dir2 / "dir5" dir5.mkdir() builder = CharmBuilder( charmdir=tmp_path, builddir=build_dir, entrypoint=entrypoint, ) # set it up to ignore some stuff and make it work builder.ignore_rules.extend_patterns( [ "dir1/dir3", "dir2/file3.txt", "dir2/dir4", ] ) builder.handle_generic_paths() assert (build_dir / "crazycharm.py").exists() assert (build_dir / "file1.txt").exists() assert (build_dir / "dir1").exists() assert not (build_dir / "dir1" / "dir3").exists() assert (build_dir / "dir2").exists() assert (build_dir / "dir2" / "file2.txt").exists() assert not (build_dir / "dir2" / "file3.txt").exists() assert not (build_dir / "dir2" / "dir4").exists() assert (build_dir / "dir2" / "dir5").exists() for (p1, p2) in [ (build_dir / "crazycharm.py", entrypoint), (build_dir / "file1.txt", file1), (build_dir / "dir2" / "file2.txt", file2), ]: if expect_hardlinks: # they're hard links assert p1.samefile(p2) else: # they're *not* hard links assert not p1.samefile(p2) # but they're essentially the same assert filecmp.cmp(str(p1), str(p2), shallow=False) assert p1.stat().st_mode == p2.stat().st_mode assert p1.stat().st_size == p2.stat().st_size assert p1.stat().st_atime == pytest.approx(p2.stat().st_atime) assert p1.stat().st_mtime == pytest.approx(p2.stat().st_mtime)