From 07fbf7268ea5f40105e3558f3471cdf08e6a0314 Mon Sep 17 00:00:00 2001 From: Nat Noordanus Date: Sat, 9 Nov 2024 19:40:24 +0200 Subject: [PATCH] fix: Fix handling of ignore_fail option in nested sequences (#253) Refs: #252 --- poethepoet/task/sequence.py | 22 +++++--- tests/test_ignore_fail.py | 100 ++++++++++++++++++++++++++++-------- 2 files changed, 96 insertions(+), 26 deletions(-) diff --git a/poethepoet/task/sequence.py b/poethepoet/task/sequence.py index e15fac57a..dbabc11bf 100644 --- a/poethepoet/task/sequence.py +++ b/poethepoet/task/sequence.py @@ -148,17 +148,27 @@ def _handle_run( ignore_fail = self.spec.options.ignore_fail non_zero_subtasks: List[str] = list() for subtask in self.subtasks: - task_result = subtask.run(context=context, parent_env=env) - if task_result and not ignore_fail: - raise ExecutionError( - f"Sequence aborted after failed subtask {subtask.name!r}" - ) + try: + task_result = subtask.run(context=context, parent_env=env) + except ExecutionError as error: + if ignore_fail: + print("Warning:", error.msg) + non_zero_subtasks.append(subtask.name) + else: + raise + if task_result: + if not ignore_fail: + raise ExecutionError( + f"Sequence aborted after failed subtask {subtask.name!r}" + ) non_zero_subtasks.append(subtask.name) if non_zero_subtasks and ignore_fail == "return_non_zero": + plural = "s" if len(non_zero_subtasks) > 1 else "" raise ExecutionError( - f"Subtasks {', '.join(non_zero_subtasks)} returned non-zero exit status" + f"Subtask{plural} {', '.join(repr(st) for st in non_zero_subtasks)} " + "returned non-zero exit status" ) return 0 diff --git a/tests/test_ignore_fail.py b/tests/test_ignore_fail.py index 28842ffd4..7c36d97d9 100644 --- a/tests/test_ignore_fail.py +++ b/tests/test_ignore_fail.py @@ -3,22 +3,30 @@ @pytest.fixture() def generate_pyproject(temp_pyproject): - """Return function which generates pyproject.toml with a given ignore_fail value.""" + def generator(lvl1_ignore_fail=False, lvl2_ignore_fail=False): + def fmt_ignore_fail(value): + if value is True: + return "ignore_fail = true" + elif isinstance(value, str): + return f'ignore_fail = "{value}"' + else: + return "" - def generator(ignore_fail): - project_tmpl = """ + project_tmpl = f""" [tool.poe.tasks] - task_1 = { shell = "echo 'task 1 error'; exit 1;" } - task_2 = { shell = "echo 'task 2 error'; exit 1;" } - task_3 = { shell = "echo 'task 3 success'; exit 0;" } + task_0 = "echo 'task 0 success'" + task_1.shell = "echo 'task 1 error'; exit 1;" + task_2.shell = "echo 'task 2 error'; exit 1;" + task_3.shell = "echo 'task 3 success'; exit 0;" - [tool.poe.tasks.all_tasks] + [tool.poe.tasks.lvl1_seq] sequence = ["task_1", "task_2", "task_3"] + {fmt_ignore_fail(lvl1_ignore_fail)} + + [tool.poe.tasks.lvl2_seq] + sequence = ["task_0", "lvl1_seq", "task_3"] + {fmt_ignore_fail(lvl2_ignore_fail)} """ - if isinstance(ignore_fail, bool) and ignore_fail: - project_tmpl += "\nignore_fail = true" - elif not isinstance(ignore_fail, bool): - project_tmpl += f'\nignore_fail = "{ignore_fail}"' return temp_pyproject(project_tmpl) @@ -27,8 +35,8 @@ def generator(ignore_fail): @pytest.mark.parametrize("fail_value", [True, "return_zero"]) def test_full_ignore(generate_pyproject, run_poe, fail_value): - project_path = generate_pyproject(ignore_fail=fail_value) - result = run_poe("all_tasks", cwd=project_path) + project_path = generate_pyproject(lvl1_ignore_fail=fail_value) + result = run_poe("lvl1_seq", cwd=project_path) assert result.code == 0, "Expected zero result" assert "task 1 error" in result.capture, "Expected first task in log" assert "task 2 error" in result.capture, "Expected second task in log" @@ -36,8 +44,8 @@ def test_full_ignore(generate_pyproject, run_poe, fail_value): def test_without_ignore(generate_pyproject, run_poe): - project_path = generate_pyproject(ignore_fail=False) - result = run_poe("all_tasks", cwd=project_path) + project_path = generate_pyproject(lvl1_ignore_fail=False) + result = run_poe("lvl1_seq", cwd=project_path) assert result.code == 1, "Expected non-zero result" assert "task 1 error" in result.capture, "Expected first task in log" assert "task 2 error" not in result.capture, "Second task shouldn't run" @@ -46,20 +54,72 @@ def test_without_ignore(generate_pyproject, run_poe): def test_return_non_zero(generate_pyproject, run_poe): - project_path = generate_pyproject(ignore_fail="return_non_zero") - result = run_poe("all_tasks", cwd=project_path) + project_path = generate_pyproject(lvl1_ignore_fail="return_non_zero") + result = run_poe("lvl1_seq", cwd=project_path) assert result.code == 1, "Expected non-zero result" assert "task 1 error" in result.capture, "Expected first task in log" assert "task 2 error" in result.capture, "Expected second task in log" assert "task 3 success" in result.capture, "Expected third task in log" - assert "Subtasks task_1, task_2 returned non-zero exit status" in result.capture + assert "Subtasks 'task_1', 'task_2' returned non-zero exit status" in result.capture def test_invalid_ignore_value(generate_pyproject, run_poe): - project_path = generate_pyproject(ignore_fail="invalid_value") - result = run_poe("all_tasks", cwd=project_path) + project_path = generate_pyproject(lvl1_ignore_fail="invalid_value") + result = run_poe("lvl1_seq", cwd=project_path) assert result.code == 1, "Expected non-zero result" assert ( "| Option 'ignore_fail' must be one of " "(True, False, 'return_zero', 'return_non_zero')\n" ) in result.capture + + +def test_nested_without_ignore(generate_pyproject, run_poe): + project_path = generate_pyproject() + result = run_poe("lvl2_seq", cwd=project_path) + assert result.code == 1, "Expected non-zero result" + assert "task 0 success" in result.capture, "Expected zeroth task in log" + assert "task 1 error" in result.capture, "Expected first task in log" + assert "task 2 error" not in result.capture, "Second task shouldn't run" + assert "task 3 success" not in result.capture, "Third task shouldn't run" + assert "Sequence aborted after failed subtask 'task_1'" in result.capture + + +def test_nested_lvl1_return_non_zero(generate_pyproject, run_poe): + project_path = generate_pyproject(lvl1_ignore_fail="return_non_zero") + result = run_poe("lvl2_seq", cwd=project_path) + assert result.code == 1, "Expected non-zero result" + assert "task 1 error" in result.capture, "Expected first task in log" + assert "task 2 error" in result.capture, "Expected second task in log" + assert "task 3 success" in result.capture, "Expected third task in log" + assert "Subtasks 'task_1', 'task_2' returned non-zero exit status" in result.capture + + +def test_nested_lvl2_return_non_zero(generate_pyproject, run_poe): + project_path = generate_pyproject(lvl2_ignore_fail="return_non_zero") + result = run_poe("lvl2_seq", cwd=project_path) + assert result.code == 1, "Expected non-zero result" + assert "task 1 error" in result.capture, "Expected first task in log" + assert "task 2 error" not in result.capture, "Expected task 2 to be skipped" + assert "task 3 success" in result.capture, "Expected third task in log" + assert ( + "Warning: Sequence aborted after failed subtask 'task_1'" in result.stdout + ) # TODO log warnings to capture + assert "Error: Subtask 'lvl1_seq' returned non-zero exit status" in result.capture + + +def test_nested_both_return_non_zero(generate_pyproject, run_poe): + project_path = generate_pyproject( + lvl1_ignore_fail="return_non_zero", lvl2_ignore_fail="return_non_zero" + ) + result = run_poe("lvl2_seq", cwd=project_path) + assert result.code == 1, "Expected non-zero result" + assert "task 1 error" in result.capture, "Expected first task in log" + assert "task 2 error" in result.capture, "Expected second task in log" + assert "task 3 success" in result.capture, "Expected third task in log" + assert ( + "Warning: Subtasks 'task_1', 'task_2' returned non-zero exit status" + in result.stdout + ) # TODO log warnings to capture + assert ( + "Error: Subtask 'lvl1_seq' returned non-zero exit status" in result.capture + ) # TODO log warnings to capture