diff --git a/src/mpyl/cli/commands/build/mpyl.py b/src/mpyl/cli/commands/build/mpyl.py index bfefc73eb..64946a5d0 100644 --- a/src/mpyl/cli/commands/build/mpyl.py +++ b/src/mpyl/cli/commands/build/mpyl.py @@ -15,7 +15,7 @@ from ....stages.discovery import for_stage, find_invalidated_projects_per_stage from ....steps.models import RunProperties from ....steps.run import RunResult -from ....steps.steps import Steps +from ....steps.steps import Steps, ExecutionException from ....utilities.repo import Repository, RepoConfig @@ -86,10 +86,10 @@ def run_mpyl(mpyl_run_parameters: MpylRunParameters, reporter: Optional[Reporter try: steps = Steps(logger=logger, properties=mpyl_run_parameters.run_config.run_properties) run_result = run_build(run_plan, steps, reporter, mpyl_run_parameters.parameters.local) - except Exception as exc: # pylint: disable=broad-except + except ExecutionException as exc: # pylint: disable=broad-except + run_result.exception = exc console.log(f'Exception during build execution: {exc}') console.print_exception() - run_result.exception = exc console.print(Markdown(run_result_to_markdown(run_result))) return run_result diff --git a/src/mpyl/reporting/formatting/markdown.py b/src/mpyl/reporting/formatting/markdown.py index 3b9bc6ebe..9163877d6 100644 --- a/src/mpyl/reporting/formatting/markdown.py +++ b/src/mpyl/reporting/formatting/markdown.py @@ -70,8 +70,10 @@ def markdown_for_stage(run_result: RunResult, stage: Stage): def run_result_to_markdown(run_result: RunResult) -> str: result: str = f'{run_result.status_line} \n' - if run_result.exception: - result += f"\n```\n{run_result.exception}\n```\n" + exception = run_result.exception + if exception: + result += f"For _{exception.executor}_ on _{exception.project_name}_ at _{exception.stage}_ \n" + result += f"\n```\n{exception}\n```\n" for stage in Stage: result += markdown_for_stage(run_result, stage) diff --git a/src/mpyl/steps/run.py b/src/mpyl/steps/run.py index 5a9971d4b..7b73a0679 100644 --- a/src/mpyl/steps/run.py +++ b/src/mpyl/steps/run.py @@ -7,14 +7,14 @@ from .models import RunProperties from ..project import Stage, Project -from .steps import StepResult +from .steps import StepResult, ExecutionException class RunResult: _run_plan: dict[Stage, set[Project]] _results: list[StepResult] _run_properties: RunProperties - _exception: Optional[Exception] + _exception: Optional[ExecutionException] def __init__(self, run_properties: RunProperties, run_plan=None): if run_plan is None: @@ -56,11 +56,11 @@ def progress_fraction(self) -> float: return 1.0 - (unfinished / total) @property - def exception(self) -> Optional[Exception]: + def exception(self) -> Optional[ExecutionException]: return self._exception @exception.setter - def exception(self, exception: Exception): + def exception(self, exception: ExecutionException): self._exception = exception @property diff --git a/src/mpyl/steps/steps.py b/src/mpyl/steps/steps.py index 18c38bb3c..46c1294f5 100644 --- a/src/mpyl/steps/steps.py +++ b/src/mpyl/steps/steps.py @@ -32,6 +32,17 @@ yaml = YAML() +class ExecutionException(Exception): + """ Exception thrown when a step execution fails. """ + + def __init__(self, project_name: str, executor: str, stage: str, message: str): + self.project_name = project_name + self.executor = executor + self.stage = stage + self.message = message + super().__init__(self.message) + + @dataclass(frozen=True) class StepResult: stage: Stage @@ -143,15 +154,23 @@ def _execute_stage(self, stage: Stage, project: Project, dry_run: bool = False) return result except Exception as exc: + message = str(exc) self._logger.warning( f"Execution of '{executor.meta.name}' for project '{project.name}' in stage {stage} " - f"failed with exception: {str(exc)}", exc_info=True) - raise ValueError from exc + f"failed with exception: {message}", exc_info=True) + raise ExecutionException(project.name, executor.meta.name, stage.name, message) from exc else: self._logger.warning(f"No executor found for {stage_name} in stage {stage}") return Output(success=False, message=f"Executor '{stage_name}' for '{stage.value}' not known or registered") def execute(self, stage: Stage, project: Project, dry_run: bool = False) -> StepResult: + """ + :param stage: the stage to execute + :param project: the project metadata + :param dry_run: indicates whether artifacts should be submitted or deployed for real + :return: StepResult + :raise ExecutionException + """ step_output = self._execute_stage(stage, project, dry_run) return StepResult(stage=stage, project=project, output=step_output) diff --git a/tests/reporting/formatting/test_resources/markdown_run_with_exception.md b/tests/reporting/formatting/test_resources/markdown_run_with_exception.md new file mode 100644 index 000000000..dbdb1c1a7 --- /dev/null +++ b/tests/reporting/formatting/test_resources/markdown_run_with_exception.md @@ -0,0 +1,10 @@ +โ— Failed with exception +For _Build SBT_ on _sbtProject_ at _Build_ + +``` +Something went wrong +``` +๐Ÿ—๏ธ _dockertest_, _test_ +๐Ÿงช _test_ +๐Ÿงช 51 โŒ 1 ๐Ÿ’” 0 ๐Ÿ™ˆ 0 [link](http://localhost/tests) +๐Ÿš€ _test_ diff --git a/tests/reporting/test_markdown_reporting.py b/tests/reporting/test_markdown_reporting.py index d602a7973..01b803028 100644 --- a/tests/reporting/test_markdown_reporting.py +++ b/tests/reporting/test_markdown_reporting.py @@ -3,7 +3,7 @@ from src.mpyl.project import Stage from src.mpyl.reporting.formatting.markdown import summary_to_markdown, run_result_to_markdown from src.mpyl.steps import Output -from src.mpyl.steps.steps import StepResult +from src.mpyl.steps.steps import StepResult, ExecutionException from src.mpyl.utilities.junit import TestRunSummary from tests import root_test_path from tests.reporting import create_test_result, create_test_result_with_plan, append_results @@ -19,6 +19,12 @@ def test_should_print_results_as_string(self): simple_report = run_result_to_markdown(run_result) assert_roundtrip(self.test_resource_path / "markdown_run.md", simple_report) + def test_should_print_exception(self): + run_result = create_test_result() + run_result.exception = ExecutionException('sbtProject', 'Build SBT', 'Build', 'Something went wrong') + simple_report = run_result_to_markdown(run_result) + assert_roundtrip(self.test_resource_path / "markdown_run_with_exception.md", simple_report) + def test_should_print_results_with_plan_as_string(self): run_result = create_test_result_with_plan() append_results(run_result)