45 lines
1.5 KiB
Python
45 lines
1.5 KiB
Python
|
|
"""Tests for subprocess.run() timeout coverage in CLI utilities."""
|
||
|
|
import ast
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
|
||
|
|
# Parameterise over every CLI module that calls subprocess.run
|
||
|
|
_CLI_MODULES = [
|
||
|
|
"hermes_cli/doctor.py",
|
||
|
|
"hermes_cli/status.py",
|
||
|
|
"hermes_cli/clipboard.py",
|
||
|
|
"hermes_cli/banner.py",
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def _subprocess_run_calls(filepath: str) -> list[dict]:
|
||
|
|
"""Parse a Python file and return info about subprocess.run() calls."""
|
||
|
|
source = Path(filepath).read_text()
|
||
|
|
tree = ast.parse(source, filename=filepath)
|
||
|
|
calls = []
|
||
|
|
for node in ast.walk(tree):
|
||
|
|
if not isinstance(node, ast.Call):
|
||
|
|
continue
|
||
|
|
func = node.func
|
||
|
|
if (isinstance(func, ast.Attribute) and func.attr == "run"
|
||
|
|
and isinstance(func.value, ast.Name)
|
||
|
|
and func.value.id == "subprocess"):
|
||
|
|
has_timeout = any(kw.arg == "timeout" for kw in node.keywords)
|
||
|
|
calls.append({"line": node.lineno, "has_timeout": has_timeout})
|
||
|
|
return calls
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("filepath", _CLI_MODULES)
|
||
|
|
def test_all_subprocess_run_calls_have_timeout(filepath):
|
||
|
|
"""Every subprocess.run() call in CLI modules must specify a timeout."""
|
||
|
|
if not Path(filepath).exists():
|
||
|
|
pytest.skip(f"{filepath} not found")
|
||
|
|
calls = _subprocess_run_calls(filepath)
|
||
|
|
missing = [c for c in calls if not c["has_timeout"]]
|
||
|
|
assert not missing, (
|
||
|
|
f"{filepath} has subprocess.run() without timeout at "
|
||
|
|
f"line(s): {[c['line'] for c in missing]}"
|
||
|
|
)
|