def test_run_actions_progress(self, tmp_path, monkeypatch, emitter): data = { "plugin": "charm", "source": ".", "charm-entrypoint": "my-entrypoint", } lifecycle = parts.PartsLifecycle( all_parts={"charm": data}, work_dir=tmp_path, project_dir=tmp_path, project_name="test", ignore_local_sources=["*.charm"], ) action1 = Action( part_name="charm", step=Step.STAGE, action_type=ActionType.RUN, reason=None ) action2 = Action( part_name="charm", step=Step.PRIME, action_type=ActionType.RUN, reason=None ) with patch("craft_parts.LifecycleManager.clean"): with patch("craft_parts.LifecycleManager.plan") as mock_plan: mock_plan.return_value = [action1, action2] with patch("craft_parts.executor.executor.ExecutionContext.execute") as mock_exec: lifecycle.run(Step.PRIME) emitter.assert_progress("Running step STAGE for part 'charm'") emitter.assert_progress("Running step PRIME for part 'charm'") assert mock_exec.call_args_list == [ call([action1], stdout=ANY, stderr=ANY), call([action2], stdout=ANY, stderr=ANY), ]
def test_run_same_entrypoint(self, tmp_path, monkeypatch): data = { "plugin": "charm", "source": ".", "charm-entrypoint": "src/charm.py", "charm-python-packages": ["pkg1", "pkg2"], "charm-requirements": ["reqs1.txt", "reqs2.txt"], } # create dispatcher from previous run prime_dir = tmp_path / "prime" prime_dir.mkdir() dispatch = prime_dir / "dispatch" dispatch.write_text( 'JUJU_DISPATCH_PATH="${JUJU_DISPATCH_PATH:-$0}" PYTHONPATH=lib:venv ./src/charm.py' ) lifecycle = parts.PartsLifecycle( all_parts={"charm": data}, work_dir=tmp_path, project_dir=tmp_path, project_name="test", ignore_local_sources=["*.charm"], ) with patch("craft_parts.LifecycleManager.clean") as mock_clean: with patch("craft_parts.LifecycleManager.plan") as mock_plan: mock_plan.side_effect = SystemExit("test") with pytest.raises(SystemExit, match="test"): lifecycle.run(Step.PRIME) mock_clean.assert_not_called()
def build_charm(self, bases_config: BasesConfiguration) -> str: """Build the charm. :param bases_config: Bases configuration to use for build. :returns: File name of charm. :raises CraftError: on lifecycle exception. :raises RuntimeError: on unexpected lifecycle exception. """ if env.is_charmcraft_running_in_managed_mode(): work_dir = env.get_managed_environment_home_path() else: work_dir = self.buildpath emit.progress(f"Building charm in {str(work_dir)!r}") if self._special_charm_part: # all current deprecated arguments set charm plugin parameters self._handle_deprecated_cli_arguments() # add charm files to the prime filter self._set_prime_filter() # set source if empty or not declared in charm part if not self._special_charm_part.get("source"): self._special_charm_part["source"] = str(self.charmdir) # run the parts lifecycle emit.trace(f"Parts definition: {self._parts}") lifecycle = parts.PartsLifecycle( self._parts, work_dir=work_dir, project_dir=self.charmdir, project_name=self.metadata.name, ignore_local_sources=["*.charm"], ) lifecycle.run(Step.PRIME) # run linters and show the results linting_results = linters.analyze(self.config, lifecycle.prime_dir) self.show_linting_results(linting_results) create_manifest( lifecycle.prime_dir, self.config.project.started_at, bases_config, linting_results, ) zipname = self.handle_package(lifecycle.prime_dir, bases_config) emit.message(f"Created '{zipname}'.", intermediate=True) return zipname
def test_prime_dir(self, tmp_path): data = { "plugin": "charm", "source": ".", } with patch("craft_parts.LifecycleManager.refresh_packages_list"): lifecycle = parts.PartsLifecycle( all_parts={"charm": data}, work_dir="/some/workdir", ignore_local_sources=["*.charm"], ) assert lifecycle.prime_dir == pathlib.Path("/some/workdir/prime")
def test_bad_bootstrap(self, tmp_path): fake_error = PartsError("pumba") with patch("craft_parts.LifecycleManager.__init__") as mock: mock.side_effect = fake_error with pytest.raises(CraftError) as cm: parts.PartsLifecycle( all_parts={}, work_dir="/some/workdir", project_dir=tmp_path, project_name="test", ignore_local_sources=["*.charm"], ) exc = cm.value assert str(exc) == "Error bootstrapping lifecycle manager: pumba" assert exc.__cause__ == fake_error
def _pack_bundle(self): """Pack a bundle.""" project = self.config.project config_parts = self.config.parts.copy() bundle_part = config_parts.setdefault("bundle", {}) prime = bundle_part.setdefault("prime", []) # get the config files bundle_filepath = project.dirpath / "bundle.yaml" bundle_config = load_yaml(bundle_filepath) if bundle_config is None: raise CommandError( "Missing or invalid main bundle file: {!r}.".format(str(bundle_filepath)) ) bundle_name = bundle_config.get("name") if not bundle_name: raise CommandError( "Invalid bundle config; missing a 'name' field indicating the bundle's name in " "file {!r}.".format(str(bundle_filepath)) ) # set prime filters for fname in MANDATORY_FILES: fpath = project.dirpath / fname if not fpath.exists(): raise CommandError("Missing mandatory file: {!r}.".format(str(fpath))) prime.extend(MANDATORY_FILES) # set source for buiding bundle_part["source"] = str(project.dirpath) # run the parts lifecycle logger.debug("Parts definition: %s", config_parts) lifecycle = parts.PartsLifecycle( config_parts, work_dir=project.dirpath / build.BUILD_DIRNAME, ignore_local_sources=[bundle_name + ".zip"], ) lifecycle.run(Step.PRIME) # pack everything create_manifest(lifecycle.prime_dir, project.started_at, None, []) zipname = project.dirpath / (bundle_name + ".zip") build_zip(zipname, lifecycle.prime_dir) logger.info("Created %r.", str(zipname))
def test_run_no_previous_entrypoint(self, tmp_path, monkeypatch): data = { "plugin": "charm", "source": ".", "charm-entrypoint": "my-entrypoint", "charm-requirements": ["reqs1.txt", "reqs2.txt"], } lifecycle = parts.PartsLifecycle( all_parts={"charm": data}, work_dir=tmp_path, ignore_local_sources=["*.charm"], ) with patch("craft_parts.LifecycleManager.clean") as mock_clean: with patch("craft_parts.LifecycleManager.plan") as mock_plan: mock_plan.side_effect = SystemExit("test") with pytest.raises(SystemExit, match="test"): lifecycle.run(Step.PRIME) mock_clean.assert_called_once_with(Step.BUILD, part_names=["charm"])
def build_charm(self, bases_config: BasesConfiguration) -> str: """Build the charm. :param bases_config: Bases configuration to use for build. :returns: File name of charm. """ logger.debug("Building charm in %r", str(self.buildpath)) self._handle_deprecated_cli_arguments() # add charm files to the prime filter self._set_prime_filter() # set source for buiding self._charm_part["source"] = str(self.charmdir) # run the parts lifecycle logger.debug("Parts definition: %s", self._parts) lifecycle = parts.PartsLifecycle( self._parts, work_dir=self.buildpath, ignore_local_sources=["*.charm"], ) lifecycle.run(Step.PRIME) # run linters and show the results linting_results = linters.analyze(self.config, lifecycle.prime_dir) self.show_linting_results(linting_results) create_manifest( lifecycle.prime_dir, self.config.project.started_at, bases_config, linting_results, ) zipname = self.handle_package(lifecycle.prime_dir, bases_config) logger.info("Created '%s'.", zipname) return zipname
def _pack_bundle(self, parsed_args) -> List[pathlib.Path]: """Pack a bundle.""" emit.progress("Packing the bundle.") if parsed_args.shell: build.launch_shell() return [] project = self.config.project if self.config.parts: config_parts = self.config.parts.copy() else: # "parts" not declared, create an implicit "bundle" part config_parts = {"bundle": {"plugin": "bundle"}} # a part named "bundle" using plugin "bundle" is special and has # predefined values set automatically. bundle_part = config_parts.get("bundle") if bundle_part and bundle_part.get("plugin") == "bundle": special_bundle_part = bundle_part else: special_bundle_part = None # get the config files bundle_filepath = project.dirpath / "bundle.yaml" bundle_config = load_yaml(bundle_filepath) if bundle_config is None: raise CraftError( "Missing or invalid main bundle file: {!r}.".format( str(bundle_filepath))) bundle_name = bundle_config.get("name") if not bundle_name: raise CraftError( "Invalid bundle config; missing a 'name' field indicating the bundle's name in " "file {!r}.".format(str(bundle_filepath))) if special_bundle_part: # set prime filters for fname in MANDATORY_FILES: fpath = project.dirpath / fname if not fpath.exists(): raise CraftError("Missing mandatory file: {!r}.".format( str(fpath))) prime = special_bundle_part.setdefault("prime", []) prime.extend(MANDATORY_FILES) # set source if empty or not declared in charm part if not special_bundle_part.get("source"): special_bundle_part["source"] = str(project.dirpath) if env.is_charmcraft_running_in_managed_mode(): work_dir = env.get_managed_environment_home_path() else: work_dir = project.dirpath / build.BUILD_DIRNAME # run the parts lifecycle emit.trace(f"Parts definition: {config_parts}") lifecycle = parts.PartsLifecycle( config_parts, work_dir=work_dir, project_dir=project.dirpath, project_name=bundle_name, ignore_local_sources=[bundle_name + ".zip"], ) try: lifecycle.run(Step.PRIME) except (RuntimeError, CraftError) as error: if parsed_args.debug: emit.trace(f"Error when running PRIME step: {error}") build.launch_shell() raise # pack everything create_manifest(lifecycle.prime_dir, project.started_at, None, []) zipname = project.dirpath / (bundle_name + ".zip") build_zip(zipname, lifecycle.prime_dir) emit.message(f"Created {str(zipname)!r}.") if parsed_args.shell_after: build.launch_shell() return [zipname]