def test_build_generics_different_filetype(tmp_path, caplog, monkeypatch): """Ignores whatever is not a regular file, symlink or dir.""" caplog.set_level(logging.DEBUG) # 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) 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 = Builder({ 'from': tmp_path, 'entrypoint': tmp_path / entrypoint, 'requirement': [], }) builder.handle_generic_paths() assert not (build_dir / 'test-socket').exists() expected = "Ignoring file because of type: 'test-socket'" 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.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder( { "from": tmp_path, "entrypoint": "whatever", "requirement": ["reqs"], }, config, ) with patch("charmcraft.commands.build._pip_needs_system") as is_bionic: is_bionic.return_value = True with patch("charmcraft.commands.build.polite_exec") as mock: mock.return_value = 0 builder.handle_dependencies() envpath = build_dir / VENV_DIRNAME assert mock.mock_calls == [ call(["pip3", "list"]), call([ "pip3", "install", "--target={}".format(envpath), "--system", "--requirement=reqs", ]), ]
def test_build_dependencies_virtualenv_multiple(tmp_path, config): """A virtualenv is created with multiple requirements files.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder( { "from": tmp_path, "entrypoint": "whatever", "requirement": ["reqs1.txt", "reqs2.txt"], }, config, ) with patch("charmcraft.commands.build.polite_exec") as mock: mock.return_value = 0 builder.handle_dependencies() envpath = build_dir / VENV_DIRNAME assert mock.mock_calls == [ call(["pip3", "list"]), call([ "pip3", "install", "--target={}".format(envpath), "--requirement=reqs1.txt", "--requirement=reqs2.txt", ]), ]
def test_build_dependencies_needs_system(tmp_path): """pip3 is called with --system when pip3 needs it.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder({ 'from': tmp_path, 'entrypoint': 'whatever', 'requirement': ['reqs'], }) with patch('charmcraft.commands.build._pip_needs_system') as is_bionic: is_bionic.return_value = True with patch('charmcraft.commands.build.polite_exec') as mock: mock.return_value = 0 builder.handle_dependencies() envpath = build_dir / VENV_DIRNAME assert mock.mock_calls == [ call(['pip3', 'list']), call([ 'pip3', 'install', '--target={}'.format(envpath), '--system', '--requirement=reqs' ]), ]
def test_build_dispatcher_classic_hooks_linking_charm_replaced( tmp_path, caplog): """Hooks that are just a symlink to the entrypoint are replaced.""" caplog.set_level(logging.DEBUG, logger="charmcraft") 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 = Builder({ 'from': tmp_path, 'entrypoint': 'whatever', 'requirement': [], }) 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" assert expected in [rec.message for rec in caplog.records]
def test_build_code_optional_bogus(tmp_path, monkeypatch): """Check that CHARM_OPTIONAL controls what gets copied.""" monkeypatch.setattr(build, 'CHARM_OPTIONAL', ['foo.yaml']) build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() entrypoint = tmp_path / 'charm.py' config = tmp_path / 'config.yaml' config.touch() foo = tmp_path / 'foo.yaml' foo.touch() builder = Builder({ 'from': tmp_path, 'entrypoint': entrypoint, 'requirement': [], }) builder.handle_code() built_config = build_dir / 'config.yaml' built_foo = build_dir / 'foo.yaml' # config.yaml is not in the build assert not built_config.exists() # but foo.yaml is assert built_foo.is_symlink() assert built_foo.resolve() == foo
def test_build_code_optional(tmp_path, optional): """Check transferred 'optional' files.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() entrypoint = tmp_path / 'charm.py' config = tmp_path / 'config.yaml' actions = tmp_path / 'actions.yaml' if 'config' in optional: config.touch() if 'actions' in optional: actions.touch() builder = Builder({ 'from': tmp_path, 'entrypoint': entrypoint, 'requirement': [], }) builder.handle_code() built_config = build_dir / 'config.yaml' built_actions = build_dir / 'actions.yaml' if 'config' in optional: assert built_config.is_symlink() assert built_config.resolve() == config else: assert not built_config.exists() if 'actions' in optional: assert built_actions.is_symlink() assert built_actions.resolve() == actions else: assert not built_actions.exists()
def test_build_code_simple(tmp_path): """Check transferred metadata and simple entrypoint.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() metadata = tmp_path / CHARM_METADATA metadata.touch() entrypoint = tmp_path / 'crazycharm.py' entrypoint.touch() builder = Builder({ 'from': tmp_path, 'entrypoint': entrypoint, 'requirement': [], }) linked_entrypoint = builder.handle_code() built_metadata = build_dir / CHARM_METADATA assert built_metadata.is_symlink() assert built_metadata.resolve() == metadata built_entrypoint = build_dir / 'crazycharm.py' assert built_entrypoint.is_symlink() assert built_entrypoint.resolve() == entrypoint assert linked_entrypoint == built_entrypoint
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.touch() entrypoint = tmp_path / 'crazycharm.py' entrypoint.touch() builder = Builder({ 'from': tmp_path, 'entrypoint': entrypoint, 'requirement': [], }) 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_generics_symlink_directory_outside(tmp_path, caplog): """Ignores (with warning) a symlink pointing a dir outside projects dir.""" caplog.set_level(logging.WARNING) project_dir = tmp_path / 'test-project' project_dir.mkdir() 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 = Builder({ 'from': project_dir, 'entrypoint': entrypoint, 'requirement': [], }) builder.handle_generic_paths() assert not (build_dir / 'external-dir').exists() expected = "Ignoring symlink because targets outside the project: 'external-dir'" assert expected in [rec.message for rec in caplog.records]
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() # 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 = Builder({ 'from': tmp_path, 'entrypoint': entrypoint, 'requirement': [], }) # 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_generics_symlink_dir(tmp_path): """Respects a symlinked dir.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() 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 = Builder({ 'from': tmp_path, 'entrypoint': entrypoint, 'requirement': [], }) 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_symlink_deep(tmp_path, config): """Correctly re-links a symlink across deep dirs.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() 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 = Builder( { "from": tmp_path, "entrypoint": entrypoint, "requirement": [], }, config, ) 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_symlink_file_outside(tmp_path, caplog, config): """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() 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 = Builder( { "from": project_dir, "entrypoint": entrypoint, "requirement": [], }, config, ) 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_dispatcher_classic_hooks_mandatory_respected(tmp_path, config): """The already included mandatory classic hooks are left untouched.""" 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 = Builder( { "from": tmp_path, "entrypoint": "whatever", "requirement": [], }, config, ) with patch("charmcraft.commands.build.MANDATORY_HOOK_NAMES", {"testhook"}): builder.handle_dispatcher(linked_entrypoint) with test_hook.open("rb") as fh: assert fh.read() == b"abc"
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() 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 = Builder({ 'from': tmp_path, 'entrypoint': entrypoint, 'requirement': [], }) 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_symlink_dir(tmp_path, config): """Respects a symlinked dir.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() 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 = Builder( { "from": tmp_path, "entrypoint": entrypoint, "requirement": [], }, config, ) 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_symlink_file(tmp_path, config): """Respects a symlinked file.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() entrypoint = tmp_path / "crazycharm.py" entrypoint.touch() the_symlink = tmp_path / "somehook.py" the_symlink.symlink_to(entrypoint) builder = Builder( { "from": tmp_path, "entrypoint": entrypoint, "requirement": [], }, config, ) 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_generics_ignored_file(tmp_path, caplog, config): """Don't include ignored filed.""" caplog.set_level(logging.DEBUG) build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() # 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 = Builder( { "from": tmp_path, "entrypoint": entrypoint, "requirement": [], }, config, ) # 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'" assert expected in [rec.message for rec in caplog.records]
def test_build_basic_complete_structure(tmp_path, monkeypatch, config): """Integration test: a simple structure with custom lib and normal src dir.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() # the metadata (save it and restore to later check) metadata_data = {"name": "name-from-metadata"} metadata_file = tmp_path / "metadata.yaml" metadata_raw = yaml.dump(metadata_data).encode("ascii") with metadata_file.open("wb") as fh: fh.write(metadata_raw) # a lib dir lib_dir = tmp_path / "lib" lib_dir.mkdir() ops_lib_dir = lib_dir / "ops" ops_lib_dir.mkdir() ops_stuff = ops_lib_dir / "stuff.txt" with ops_stuff.open("wb") as fh: fh.write(b"ops stuff") # simple source code src_dir = tmp_path / "src" src_dir.mkdir() charm_script = src_dir / "charm.py" with charm_script.open("wb") as fh: fh.write(b"all the magic") monkeypatch.chdir(tmp_path) # so the zip file is left in the temp dir builder = Builder( { "from": tmp_path, "entrypoint": charm_script, "requirement": [], }, config, ) zipname = builder.run() # check all is properly inside the zip # contents!), and all relative to build dir zf = zipfile.ZipFile(zipname) assert zf.read("metadata.yaml") == metadata_raw assert zf.read("src/charm.py") == b"all the magic" dispatch = DISPATCH_CONTENT.format( entrypoint_relative_path="src/charm.py").encode("ascii") assert zf.read("dispatch") == dispatch assert zf.read("hooks/install") == dispatch assert zf.read("hooks/start") == dispatch assert zf.read("hooks/upgrade-charm") == dispatch assert zf.read("lib/ops/stuff.txt") == b"ops stuff" # check the manifest is present and with particular values that depend on given info manifest = yaml.safe_load(zf.read("manifest.yaml")) assert (manifest["charmcraft-started-at"] == config.project.started_at.isoformat() + "Z")
def test_build_basic_complete_structure(tmp_path, monkeypatch, config): """Integration test: a simple structure with custom lib and normal src dir.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() # the metadata (save it and restore to later check) metadata_data = {'name': 'name-from-metadata'} metadata_file = tmp_path / 'metadata.yaml' metadata_raw = yaml.dump(metadata_data).encode('ascii') with metadata_file.open('wb') as fh: fh.write(metadata_raw) # a lib dir lib_dir = tmp_path / 'lib' lib_dir.mkdir() ops_lib_dir = lib_dir / 'ops' ops_lib_dir.mkdir() ops_stuff = ops_lib_dir / 'stuff.txt' with ops_stuff.open('wb') as fh: fh.write(b'ops stuff') # simple source code src_dir = tmp_path / 'src' src_dir.mkdir() charm_script = src_dir / 'charm.py' with charm_script.open('wb') as fh: fh.write(b'all the magic') monkeypatch.chdir(tmp_path) # so the zip file is left in the temp dir builder = Builder( { 'from': tmp_path, 'entrypoint': charm_script, 'requirement': [], }, config) zipname = builder.run() # check all is properly inside the zip # contents!), and all relative to build dir zf = zipfile.ZipFile(zipname) assert zf.read('metadata.yaml') == metadata_raw assert zf.read('src/charm.py') == b"all the magic" dispatch = DISPATCH_CONTENT.format( entrypoint_relative_path='src/charm.py').encode('ascii') assert zf.read('dispatch') == dispatch assert zf.read('hooks/install') == dispatch assert zf.read('hooks/start') == dispatch assert zf.read('hooks/upgrade-charm') == dispatch assert zf.read('lib/ops/stuff.txt') == b"ops stuff" # check the manifest is present and with particular values that depend on given info manifest = yaml.safe_load(zf.read('manifest.yaml')) assert manifest[ 'charmcraft-started-at'] == config.project.started_at.isoformat() + "Z"
def test_builder_without_jujuignore(tmp_path): """Without a .jujuignore we still have a default set of ignores""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder({ 'from': tmp_path, 'entrypoint': 'whatever', 'requirement': [], }) 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_basic_complete_structure(tmp_path, monkeypatch): """Integration test: a simple structure with custom lib and normal src dir.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() # the metadata (save it and restore to later check) metadata_data = {'name': 'name-from-metadata'} metadata_file = tmp_path / 'metadata.yaml' metadata_raw = yaml.dump(metadata_data).encode('ascii') with metadata_file.open('wb') as fh: fh.write(metadata_raw) # a lib dir lib_dir = tmp_path / 'lib' lib_dir.mkdir() ops_lib_dir = lib_dir / 'ops' ops_lib_dir.mkdir() ops_stuff = ops_lib_dir / 'stuff.txt' with ops_stuff.open('wb') as fh: fh.write(b'ops stuff') # simple source code src_dir = tmp_path / 'src' src_dir.mkdir() charm_script = src_dir / 'charm.py' with charm_script.open('wb') as fh: fh.write(b'all the magic') monkeypatch.chdir(tmp_path) # so the zip file is left in the temp dir builder = Builder({ 'from': pathlib.Path( str(tmp_path)), # bad support for tmp_path's pathlib2 in Py3.5 'entrypoint': pathlib.Path(str(charm_script)), 'requirement': [], }) zipname = builder.run() # check all is properly inside the zip # contents!), and all relative to build dir zf = zipfile.ZipFile(zipname) assert zf.read('metadata.yaml') == metadata_raw assert zf.read('src/charm.py') == b"all the magic" dispatch = DISPATCH_CONTENT.format( entrypoint_relative_path='src/charm.py').encode('ascii') assert zf.read('dispatch') == dispatch assert zf.read('hooks/install') == dispatch assert zf.read('hooks/start') == dispatch assert zf.read('hooks/upgrade-charm') == dispatch assert zf.read('lib/ops/stuff.txt') == b"ops stuff"
def test_build_dependencies_virtualenv_none(tmp_path): """The virtualenv is NOT created if no needed.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder({ 'from': tmp_path, 'entrypoint': 'whatever', 'requirement': [], }) with patch('charmcraft.commands.build.polite_exec') as mock: builder.handle_dependencies() mock.assert_not_called()
def test_build_dependencies_virtualenv_error_installing(tmp_path): """Process is properly interrupted if virtualenv creation fails.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder({ 'from': tmp_path, 'entrypoint': 'whatever', 'requirement': ['something'], }) with patch('charmcraft.commands.build.polite_exec') as mock: mock.side_effect = [0, -7] with pytest.raises(CommandError, match="problems installing dependencies"): builder.handle_dependencies()
def test_build_dependencies_virtualenv_error_basicpip(tmp_path): """Process is properly interrupted if using pip fails.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder({ 'from': tmp_path, 'entrypoint': 'whatever', 'requirement': ['something'], }) with patch('charmcraft.commands.build.polite_exec') as mock: mock.return_value = -7 with pytest.raises(CommandError, match="problems using pip"): builder.handle_dependencies()
def test_build_code_includes_templates(tmp_path): """If 'templates' exists, it is included in the build tree.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() source_dir = tmp_path / "templates" entrypoint = tmp_path / 'charm.py' source_dir.mkdir() builder = Builder({ 'from': tmp_path, 'entrypoint': entrypoint, 'requirement': [], }) builder.handle_code() built_dir = build_dir / 'templates' assert built_dir.is_symlink() assert built_dir.resolve() == source_dir
def test_builder_without_jujuignore(tmp_path, config): """Without a .jujuignore we still have a default set of ignores""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder( { "from": tmp_path, "entrypoint": "whatever", "requirement": [], }, config, ) 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_dispatcher_modern_dispatch_respected(tmp_path): """The already included dispatcher script is left untouched.""" 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 = Builder({ 'from': tmp_path, 'entrypoint': 'whatever', 'requirement': [], }) builder.handle_dispatcher('whatever') with already_present_dispatch.open('rb') as fh: assert fh.read() == b'abc'
def test_build_dependencies_virtualenv_error_basicpip(tmp_path, config): """Process is properly interrupted if using pip fails.""" build_dir = tmp_path / BUILD_DIRNAME build_dir.mkdir() builder = Builder( { "from": tmp_path, "entrypoint": "whatever", "requirement": ["something"], }, config, ) with patch("charmcraft.commands.build.polite_exec") as mock: mock.return_value = -7 with pytest.raises(CommandError, match="problems using pip"): builder.handle_dependencies()