Skip to content

Commit

Permalink
fix: Fix handling of ignore_fail option in nested sequences (#253)
Browse files Browse the repository at this point in the history
Refs: #252
  • Loading branch information
nat-n committed Nov 9, 2024
1 parent d895911 commit 44de00f
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 26 deletions.
22 changes: 16 additions & 6 deletions poethepoet/task/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
100 changes: 80 additions & 20 deletions tests/test_ignore_fail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -27,17 +35,17 @@ 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"
assert "task 3 success" in result.capture, "Expected third task in log"


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"
Expand All @@ -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

0 comments on commit 44de00f

Please sign in to comment.