Spaces:
Running
Running
File size: 9,542 Bytes
e7c4b2b | 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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | """
scripts/local_eval.py (v2)
ββββββββββββββββββββββββββββ
Real SWE-bench evaluation WITHOUT Docker.
Key improvements over v1:
- Clones each repo ONCE and reuses via git stash/checkout
- Properly fetches specific commits
- Handles both patch formats (string & dict)
- Saves disk by not re-cloning for every issue
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
from datetime import datetime, timezone
# ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
EVAL_INPUT = Path(os.environ.get("EVAL_INPUT", "results/eval/patches_to_eval.jsonl"))
RESULTS_OUT = Path("results/eval/local_eval_results.jsonl")
REPO_CACHE = Path("results/eval/repo_cache") # repos cloned here once
MAX_ISSUES = int(os.environ.get("MAX_EVAL", "16"))
RESULTS_OUT.parent.mkdir(parents=True, exist_ok=True)
REPO_CACHE.mkdir(parents=True, exist_ok=True)
# ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def run(cmd, cwd=None, timeout=600, input_bytes=None):
return subprocess.run(cmd, cwd=cwd, capture_output=True,
timeout=timeout, input=input_bytes)
def ensure_repo(repo: str) -> Path:
"""Clone repo once into REPO_CACHE. Re-uses if already there."""
repo_dir = REPO_CACHE / repo.replace("/", "__")
if not repo_dir.exists():
url = f"https://github.com/{repo}.git"
print(f" Cloning {repo} (one-time, ~1-2 min)...", flush=True)
run(["git", "clone", url, str(repo_dir)], timeout=900)
return repo_dir
def checkout_commit(repo_dir: Path, commit: str) -> bool:
"""Fetch + checkout a specific commit."""
# Reset any changes first
run(["git", "checkout", "."], cwd=repo_dir, timeout=30)
run(["git", "clean", "-fd"], cwd=repo_dir, timeout=30)
# Try direct checkout
r = run(["git", "checkout", commit], cwd=repo_dir, timeout=30)
if r.returncode == 0:
return True
# Fetch and retry
print(f" Fetching commit {commit[:8]}...", flush=True)
run(["git", "fetch", "origin", commit], cwd=repo_dir, timeout=120)
run(["git", "fetch", "--all"], cwd=repo_dir, timeout=300)
r = run(["git", "checkout", commit], cwd=repo_dir, timeout=30)
return r.returncode == 0
def install_deps(repo_dir: Path):
run([sys.executable, "-m", "pip", "install", "-e", ".",
"--quiet", "--no-deps", "--no-build-isolation"],
cwd=repo_dir, timeout=180)
def extract_patch(patch_field) -> str:
"""Handle both string and dict patch formats."""
if isinstance(patch_field, dict):
return patch_field.get("patch", "") or ""
return patch_field or ""
def apply_patch(patch_text: str, repo_dir: Path) -> bool:
if not patch_text.strip():
return False
r = run(["git", "apply", "--whitespace=fix", "-"],
cwd=repo_dir, input_bytes=patch_text.encode(), timeout=30)
if r.returncode != 0:
# Try with --reject (partial apply info)
r2 = run(["git", "apply", "--check", "-"],
cwd=repo_dir, input_bytes=patch_text.encode(), timeout=30)
print(f" Patch check stderr: {r2.stderr.decode()[:200]}", flush=True)
return r.returncode == 0
def run_tests(repo_dir: Path, test_ids: list[str], timeout=90) -> tuple[bool, str]:
if not test_ids:
return True, "(no tests)"
cmd = [sys.executable, "-m", "pytest", "-v", "--tb=short",
"--no-header", "-x", "--timeout=30"] + test_ids
try:
r = subprocess.run(cmd, capture_output=True, text=True,
cwd=repo_dir, timeout=timeout)
out = r.stdout + r.stderr
return r.returncode == 0, out[-2000:]
except subprocess.TimeoutExpired:
return False, "TIMEOUT"
except Exception as e:
return False, str(e)
def load_instance(instance_id: str) -> dict | None:
"""Load SWE-bench instance from HF dataset."""
try:
from datasets import load_dataset
ds = load_dataset("princeton-nlp/SWE-bench_Lite", split="test",
trust_remote_code=True)
for row in ds:
if row["instance_id"] == instance_id:
return dict(row)
except Exception as e:
print(f" datasets error: {e}", flush=True)
return None
# ββ Main evaluator ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def evaluate(patch_rec: dict) -> dict:
iid = patch_rec["instance_id"]
print(f"\n{'β'*60}")
print(f"βΊ {iid}", flush=True)
patch_text = extract_patch(patch_rec.get("patch", ""))
if not patch_text.strip():
print(" β Empty patch β skipping")
return {**patch_rec, "eval_resolved": False, "eval_error": "empty patch"}
# Fetch ground truth
print(" Loading SWE-bench instance...", flush=True)
instance = load_instance(iid)
if not instance:
return {**patch_rec, "eval_resolved": False, "eval_error": "HF dataset load failed"}
base_commit = instance["base_commit"]
fail_to_pass = json.loads(instance.get("FAIL_TO_PASS", "[]"))
pass_to_pass = json.loads(instance.get("PASS_TO_PASS", "[]"))
# Limit pass_to_pass to 3 to save time
test_ids = fail_to_pass + pass_to_pass[:3]
print(f" Commit: {base_commit[:10]} | "
f"F2P: {len(fail_to_pass)} | P2P: {len(pass_to_pass[:3])}", flush=True)
# Get/reuse cached repo
repo_dir = ensure_repo(patch_rec["repo"])
# Checkout exact commit
print(f" Checking out {base_commit[:8]}...", flush=True)
if not checkout_commit(repo_dir, base_commit):
return {**patch_rec, "eval_resolved": False,
"eval_error": f"checkout {base_commit[:8]} failed"}
# Install at this commit
install_deps(repo_dir)
# Apply patch
print(" Applying patch...", flush=True)
if not apply_patch(patch_text, repo_dir):
# Reset and report
run(["git", "checkout", "."], cwd=repo_dir, timeout=30)
return {**patch_rec, "eval_resolved": False,
"eval_error": "patch did not apply", "eval_base_commit": base_commit}
# Run tests
print(f" Running {len(test_ids)} tests...", flush=True)
passed, output = run_tests(repo_dir, test_ids)
# Reset repo
run(["git", "checkout", "."], cwd=repo_dir, timeout=30)
run(["git", "clean", "-fd"], cwd=repo_dir, timeout=30)
status = "β
RESOLVED" if passed else "β NOT RESOLVED"
print(f" β {status}")
return {
**patch_rec,
"eval_resolved": passed,
"eval_base_commit": base_commit,
"eval_patch_ok": True,
"eval_fail_to_pass": fail_to_pass,
"eval_output": output[-400:],
"eval_ts": datetime.now(timezone.utc).isoformat(),
}
def main():
print("π¬ SWE-bench Local Evaluation (v2 β repo cache)")
print(f" Input: {EVAL_INPUT}")
print(f" Output: {RESULTS_OUT}")
print(f" Limit: {MAX_ISSUES} issues\n")
with open(EVAL_INPUT) as f:
patches = [json.loads(l) for l in f if l.strip()][:MAX_ISSUES]
# Load already done
done_ids, eval_results = set(), []
if RESULTS_OUT.exists():
with open(RESULTS_OUT) as f:
for line in f:
if line.strip():
r = json.loads(line)
done_ids.add(r["instance_id"])
eval_results.append(r)
print(f"Resuming: {len(done_ids)} done, {len(patches)-len(done_ids)} to go\n")
# Install datasets if needed
try:
import datasets # noqa
except ImportError:
subprocess.run([sys.executable, "-m", "pip", "install", "datasets", "-q"])
resolved = sum(1 for r in eval_results if r.get("eval_resolved"))
for i, patch in enumerate(patches):
iid = patch["instance_id"]
if iid in done_ids:
print(f"[{i+1}/{len(patches)}] SKIP {iid}")
continue
print(f"\n[{i+1}/{len(patches)}]", end="", flush=True)
result = evaluate(patch)
eval_results.append(result)
done_ids.add(iid)
if result.get("eval_resolved"):
resolved += 1
with open(RESULTS_OUT, "a") as f:
f.write(json.dumps(result) + "\n")
done_count = len([r for r in eval_results if "eval_resolved" in r])
print(f" Score so far: {resolved}/{done_count} "
f"({100*resolved//max(done_count,1)}%)\n", flush=True)
# Summary
evaluated = [r for r in eval_results if "eval_resolved" in r]
resolved_list = [r for r in evaluated if r.get("eval_resolved")]
print("\n" + "="*60)
print("FINAL RESULTS")
print("="*60)
for r in evaluated:
s = "β
" if r.get("eval_resolved") else "β"
err = f" [{r.get('eval_error','')}]" if r.get("eval_error") else ""
print(f" {s} {r['instance_id']}{err}")
print()
print(f"Resolved: {len(resolved_list)}/{len(evaluated)} "
f"({100*len(resolved_list)//max(len(evaluated),1)}%)")
print(f"Saved to: {RESULTS_OUT}")
if __name__ == "__main__":
main()
|