Spaces:
Sleeping
Sleeping
File size: 3,397 Bytes
b51092e 8e26883 16ef9ea f8b17b7 007ba8f b51092e 007ba8f f8b17b7 b51092e 2b92050 007ba8f b51092e 3c64c1d 8e26883 3c64c1d decea2b 3c64c1d b51092e 007ba8f b51092e decea2b b51092e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | """
Runs pytest against a cloned repository inside a resource-limited sandbox.
"""
import os
import sys
import subprocess # nosec B404
from typing import Any
from app.core.logger import get_logger
from app.tools.sandbox import run_sandboxed
logger = get_logger(__name__)
# Hard cap on how many test output lines we forward to the LLM
_MAX_OUTPUT_LINES = 100
def run_tests(local_path: str) -> dict[str, Any]:
"""
Execute pytest in local_path inside the sandbox and return a summary.
Returns:
{
"passed": int,
"failed": int,
"errors": int,
"output": str, # truncated pytest stdout/stderr
"timed_out": bool,
"oom_killed": bool,
}
"""
summary: dict[str, Any] = {
"passed": 0,
"failed": 0,
"errors": 0,
"output": "",
"timed_out": False,
"oom_killed": False,
"success": False,
}
# Minimal safe env: inherit PATH so pytest is findable, but strip
# secrets that might be present in the parent environment
safe_env = {
"PATH": os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin"),
"HOME": "/tmp", # nosec B108 — restrict home dir
"PYTHONDONTWRITEBYTECODE": "1",
}
try:
result = run_sandboxed(
cmd=[sys.executable, "-m", "pytest", "--tb=short", "-q", "--no-header"],
cwd=local_path,
timeout=120,
env=safe_env,
)
except subprocess.TimeoutExpired:
summary["output"] = "pytest timed out"
summary["timed_out"] = True
return summary
summary["timed_out"] = result["timed_out"]
summary["oom_killed"] = result["oom_killed"]
raw_output = (result["stdout"] + result["stderr"]).strip()
# Truncate to avoid flooding the LLM context
lines = raw_output.splitlines()
if len(lines) > _MAX_OUTPUT_LINES:
lines = lines[:_MAX_OUTPUT_LINES] + [f"... ({len(lines) - _MAX_OUTPUT_LINES} lines truncated)"]
summary["output"] = "\n".join(lines)
# Parse pytest's summary line: "3 passed, 1 failed, 2 errors"
for line in lines:
lower = line.lower()
if "passed" in lower or "failed" in lower or "error" in lower:
summary["passed"] += _parse_count(line, "passed")
summary["failed"] += _parse_count(line, "failed")
summary["errors"] += _parse_count(line, "error")
if result["timed_out"]:
logger.warning("pytest timed out", extra={"path": local_path})
elif result["oom_killed"]:
logger.warning("pytest was OOM-killed", extra={"path": local_path})
else:
logger.info(
"pytest complete",
extra={
"passed": summary["passed"],
"failed": summary["failed"],
"errors": summary["errors"],
},
)
summary["success"] = (
not summary["timed_out"]
and not summary["oom_killed"]
and summary["failed"] == 0
and summary["errors"] == 0
and summary["passed"] > 0
)
return summary
def _parse_count(line: str, keyword: str) -> int:
"""Extract the integer before `keyword` in a pytest summary line."""
import re
match = re.search(rf"(\d+)\s+{keyword}", line.lower())
return int(match.group(1)) if match else 0 |