refactor(cli): 移动CliRunner到顶层runner模块并清理冗余代码
This commit is contained in:
@@ -491,7 +491,7 @@ class TestCliRunnerErrorHandling:
|
||||
def raise_interrupt(*args: Any, **kwargs: Any) -> None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
with patch("pyflowx.cli.runner.run", side_effect=raise_interrupt):
|
||||
with patch("pyflowx.runner.run", side_effect=raise_interrupt):
|
||||
exit_code = runner.run(["echo"])
|
||||
assert exit_code == CliExitCode.INTERRUPTED.value
|
||||
captured = capsys.readouterr()
|
||||
@@ -506,7 +506,7 @@ class TestCliRunnerErrorHandling:
|
||||
def raise_error(*args: Any, **kwargs: Any) -> None:
|
||||
raise TaskFailedError("echo", RuntimeError("boom"), 1)
|
||||
|
||||
with patch("pyflowx.cli.runner.run", side_effect=raise_error):
|
||||
with patch("pyflowx.runner.run", side_effect=raise_error):
|
||||
exit_code = runner.run(["echo"])
|
||||
assert exit_code == CliExitCode.FAILURE.value
|
||||
captured = capsys.readouterr()
|
||||
@@ -523,7 +523,7 @@ class TestCliRunnerErrorHandling:
|
||||
def raise_custom(*args: Any, **kwargs: Any) -> None:
|
||||
raise CustomError("unexpected")
|
||||
|
||||
with patch("pyflowx.cli.runner.run", side_effect=raise_custom):
|
||||
with patch("pyflowx.runner.run", side_effect=raise_custom):
|
||||
with pytest.raises(CustomError):
|
||||
runner.run(["echo"])
|
||||
|
||||
|
||||
@@ -348,5 +348,194 @@ def test_taskspec_callable_cmd():
|
||||
assert report.results["callable_cmd"].value == "callable result"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# verbose 模式测试
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestTaskSpecVerbose:
|
||||
"""测试 TaskSpec 的 verbose 字段."""
|
||||
|
||||
def test_verbose_default_is_false(self) -> None:
|
||||
"""verbose 默认应为 False."""
|
||||
spec = px.TaskSpec("a", cmd=[*ECHO_CMD, "hi"])
|
||||
assert spec.verbose is False
|
||||
|
||||
def test_verbose_true_prints_command(
|
||||
self, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""verbose=True 时应打印执行的命令."""
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("echo", cmd=[*ECHO_CMD, "verbose-output"], verbose=True)]
|
||||
)
|
||||
px.run(graph, strategy="sequential")
|
||||
captured = capsys.readouterr()
|
||||
assert "执行命令" in captured.out
|
||||
assert "返回码" in captured.out
|
||||
|
||||
def test_verbose_false_silent(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""verbose=False 时不应打印命令信息."""
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("echo", cmd=[*ECHO_CMD, "silent"], verbose=False)]
|
||||
)
|
||||
px.run(graph, strategy="sequential")
|
||||
captured = capsys.readouterr()
|
||||
assert "执行命令" not in captured.out
|
||||
assert "返回码" not in captured.out
|
||||
|
||||
def test_verbose_true_shell_cmd(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""verbose=True 时 shell 命令也应打印执行信息."""
|
||||
if sys.platform == "win32":
|
||||
shell_cmd = 'cmd /c "echo shell-verbose"'
|
||||
else:
|
||||
shell_cmd = "echo 'shell-verbose'"
|
||||
|
||||
graph = px.Graph.from_specs([px.TaskSpec("shell", cmd=shell_cmd, verbose=True)])
|
||||
px.run(graph, strategy="sequential")
|
||||
captured = capsys.readouterr()
|
||||
assert "执行 Shell" in captured.out
|
||||
|
||||
def test_verbose_prints_cwd(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""verbose=True 且设置了 cwd 时应打印工作目录."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("ls", cmd=ECHO_CMD, cwd=Path(tmpdir), verbose=True)]
|
||||
)
|
||||
px.run(graph, strategy="sequential")
|
||||
captured = capsys.readouterr()
|
||||
assert "工作目录" in captured.out
|
||||
|
||||
def test_verbose_failure_includes_returncode(
|
||||
self, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""verbose=True 时失败也应打印返回码."""
|
||||
from pyflowx.errors import TaskFailedError
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"fail",
|
||||
cmd=["python", "-c", "import sys; sys.exit(1)"],
|
||||
verbose=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
with pytest.raises(TaskFailedError):
|
||||
px.run(graph, strategy="sequential")
|
||||
captured = capsys.readouterr()
|
||||
assert "返回码" in captured.out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# _wrap_cmd 错误路径测试
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestTaskSpecCmdErrors:
|
||||
"""测试 _wrap_cmd 的错误处理路径."""
|
||||
|
||||
def test_cmd_list_file_not_found(self) -> None:
|
||||
"""命令不存在时应抛出 RuntimeError."""
|
||||
from pyflowx.errors import TaskFailedError
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("missing", cmd=["this-command-does-not-exist-xyz"])]
|
||||
)
|
||||
with pytest.raises(TaskFailedError) as exc_info:
|
||||
px.run(graph, strategy="sequential")
|
||||
# 错误信息应包含命令未找到
|
||||
assert (
|
||||
"命令未找到" in str(exc_info.value.cause)
|
||||
or "not found" in str(exc_info.value.cause).lower()
|
||||
)
|
||||
|
||||
def test_cmd_list_failure_includes_stderr(self) -> None:
|
||||
"""命令失败时错误信息应包含 stderr."""
|
||||
from pyflowx.errors import TaskFailedError
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"fail",
|
||||
cmd=[
|
||||
"python",
|
||||
"-c",
|
||||
"import sys; sys.stderr.write('error-msg'); sys.exit(1)",
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
with pytest.raises(TaskFailedError) as exc_info:
|
||||
px.run(graph, strategy="sequential")
|
||||
# 非 verbose 模式下, stderr 应包含在错误信息中
|
||||
assert "error-msg" in str(exc_info.value.cause)
|
||||
|
||||
def test_cmd_string_file_not_found(self) -> None:
|
||||
"""shell 命令不存在时应抛出 RuntimeError."""
|
||||
from pyflowx.errors import TaskFailedError
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("missing", cmd="this-command-does-not-exist-xyz-123")]
|
||||
)
|
||||
with pytest.raises(TaskFailedError):
|
||||
px.run(graph, strategy="sequential")
|
||||
|
||||
def test_cmd_string_failure(self) -> None:
|
||||
"""shell 命令失败时应抛出 RuntimeError."""
|
||||
from pyflowx.errors import TaskFailedError
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("fail", cmd='python -c "import sys; sys.exit(1)"')]
|
||||
)
|
||||
with pytest.raises(TaskFailedError) as exc_info:
|
||||
px.run(graph, strategy="sequential")
|
||||
assert "Shell 命令执行失败" in str(exc_info.value.cause)
|
||||
|
||||
def test_cmd_timeout_raises(self) -> None:
|
||||
"""命令超时应抛出 RuntimeError."""
|
||||
from pyflowx.errors import TaskFailedError
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"slow",
|
||||
cmd=["python", "-c", "import time; time.sleep(5)"],
|
||||
timeout=0.1,
|
||||
)
|
||||
]
|
||||
)
|
||||
with pytest.raises(TaskFailedError) as exc_info:
|
||||
px.run(graph, strategy="sequential")
|
||||
assert "超时" in str(exc_info.value.cause)
|
||||
|
||||
def test_cmd_string_timeout_raises(self) -> None:
|
||||
"""shell 命令超时应抛出 RuntimeError."""
|
||||
from pyflowx.errors import TaskFailedError
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"slow", cmd='python -c "import time; time.sleep(5)"', timeout=0.1
|
||||
)
|
||||
]
|
||||
)
|
||||
with pytest.raises(TaskFailedError) as exc_info:
|
||||
px.run(graph, strategy="sequential")
|
||||
assert "超时" in str(exc_info.value.cause)
|
||||
|
||||
def test_unsupported_cmd_type_raises(self) -> None:
|
||||
"""不支持的 cmd 类型应在执行时抛出 TypeError."""
|
||||
from pyflowx.errors import TaskFailedError
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("bad", cmd=123)] # type: ignore[arg-type]
|
||||
)
|
||||
with pytest.raises((TypeError, TaskFailedError)):
|
||||
px.run(graph, strategy="sequential")
|
||||
|
||||
def test_no_fn_no_cmd_raises(self) -> None:
|
||||
"""没有 fn 和 cmd 时应抛出 ValueError."""
|
||||
with pytest.raises(ValueError, match="必须提供 fn 或 cmd"):
|
||||
px.TaskSpec("empty")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
Reference in New Issue
Block a user