Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix handling of ignore_fail option in nested sequences #253

Merged
merged 1 commit into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading