File size: 11,714 Bytes
9438a57 | 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 | #!/usr/bin/env python3
"""QuixBugs'tan bağımsız (üçüncü kişilerce yazılmış) bir veri seti bölümü oluşturur.
QuixBugs (Lin ve ark., 2017; MIT lisansı) Quixey Challenge'ın 40 klasik algoritmasını Python ve Java'da,
her biri tek satırlık bir hatayla sunar. Bu betik:
* doğru sürümü test edilecek kod olarak alır (Python açıklama dizgisi Java'ya Javadoc olarak eklenir),
* doğru ↔ hatalı sürüm farkından "yerleştirilmiş hata" yamasını otomatik çıkarır,
* QuixBugs'ın kendi test vakalarını referans paket olarak dönüştürür (pytest / JUnit 4 ve 5),
* her öğeyi doğrular (referans doğru kodda tamamen yeşil, hatalı kodda en az bir test başarısız),
* aynı algoritma iki kez sayılmasın diye farklı programları Python ve Java arasında paylaştırır.
git clone --depth 1 https://github.com/jkoppel/QuixBugs /tmp/QuixBugs
python scripts/import_quixbugs.py /tmp/QuixBugs --per-language 10 --seed 2026
"""
from __future__ import annotations
import argparse
import difflib
import json
import random
import re
import shutil
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "backend"))
from app.sandbox.registry import get_runner # noqa: E402
OUT = ROOT / "benchmarks" / "quixbugs"
SOURCE = {"dataset": "QuixBugs", "url": "https://github.com/jkoppel/QuixBugs", "license": "MIT",
"citation": "D. Lin, J. Koppel, A. Chen, A. Solar-Lezama, SPLASH Companion 2017"}
DOC_RE = re.compile(r'\n\s*"""(.*?)"""\s*$', re.S)
def docstring(buggy_py: str) -> tuple[str, str]:
"""QuixBugs Python dosyalarında açıklama dizgisi kodun sonundadır: (kod, açıklama)."""
m = DOC_RE.search(buggy_py)
return (buggy_py[:m.start()].rstrip() + "\n", m.group(1).strip()) if m else (buggy_py, "")
def one_hunk(correct: str, buggy: str) -> tuple[str, str] | None:
"""Doğru → hatalı tek bir değişiklik bloğu varsa (from, to) döndürür; boşluk farkları yok sayılır."""
a, b = correct.splitlines(), buggy.splitlines()
sm = difflib.SequenceMatcher(None, [x.strip() for x in a], [x.strip() for x in b], autojunk=False)
# yalnızca boş satırlardan oluşan farklar (ör. dosya başındaki fazladan boş satır) yok sayılır
hunks = [op for op in sm.get_opcodes() if op[0] != "equal"
and any(x.strip() for x in a[op[1]:op[2]] + b[op[3]:op[4]])]
if len(hunks) != 1:
return None
_, i1, i2, j1, j2 = hunks[0]
if i1 == i2: # hatalı sürümde eklenmiş satır: bağlam için önceki satırı da al
if i1 == 0:
return None
i1 -= 1
j1 -= 1
frm, to = "\n".join(a[i1:i2]), "\n".join(b[j1:j2])
k = i1
while correct.count(frm) != 1 and k > 0: # benzersiz olana kadar yukarı doğru bağlam ekle
k -= 1
frm, to = a[k] + "\n" + frm, a[k] + "\n" + to
return (frm, to) if correct.count(frm) == 1 and frm != to else None
def python_item(qb: Path, name: str):
buggy = (qb / "python_programs" / f"{name}.py").read_text()
correct = (qb / "correct_python_programs" / f"{name}.py").read_text()
_, doc = docstring(buggy)
code_correct, _ = docstring(correct)
code = code_correct.rstrip() + ('\n\n\n"""\n' + doc + '\n"""\n' if doc else "\n")
buggy_code, _ = docstring(buggy)
patch = one_hunk(code_correct, buggy_code)
tpath = qb / "python_testcases" / f"test_{name}.py"
jpath = qb / "json_testcases" / f"{name}.json"
if not patch or not doc or not tpath.exists() or not jpath.exists():
return None
t = tpath.read_text()
if "load_json_testcases" not in t:
return None
cases = [json.loads(ln) for ln in jpath.read_text().splitlines() if ln.strip()]
t = t.replace("from load_testdata import load_json_testcases\n", "")
t = re.sub(r"if pytest\.use_correct:\n\s+from correct_python_programs\.\w+ import (\w+)\nelse:\n"
r"\s+from python_programs\.\w+ import \w+\n", r"from solution import \1\n", t)
t = re.sub(r"load_json_testcases\([\w.]+\)", repr(cases), t)
t = t.replace("pytest.run_slow", "False")
if "python_programs" in t or "use_correct" in t:
return None
return {"code": code, "tests": t, "patch": patch, "ext": "py", "file": f"{name}.py"}
def java_item(qb: Path, name: str, doc: str):
up = name.upper()
cpath, bpath = qb / "correct_java_programs" / f"{up}.java", qb / "java_programs" / f"{up}.java"
tpath = qb / "java_testcases" / "junit" / f"{up}_TEST.java"
if not (cpath.exists() and bpath.exists() and tpath.exists()):
return None
strip_pkg = lambda s: re.sub(r"^\s*package\s+[\w.]+;\s*\n", "", s, flags=re.M) # noqa: E731
strip_tpl = lambda s: re.sub(r"/\*\s*\n \* To change this template.*?\*/\s*\n", "", s, flags=re.S) # noqa: E731
correct, buggy = strip_tpl(strip_pkg(cpath.read_text())), strip_tpl(strip_pkg(bpath.read_text()))
patch = one_hunk(correct, buggy)
t = tpath.read_text()
if not patch or re.search(r"\b(Node|WeightedEdge)\b", correct + t):
return None
# özgün dosyadaki yalnızca @author içeren Javadoc bloğu, açıklamayla tek blokta birleştirilir
am = re.search(r"/\*\*\s*\n\s*\*\s*\n\s*\*\s*@author\s+(\S+)\s*\n\s*\*/\s*\n", correct)
author = am.group(1) if am else ""
if am:
correct = correct[:am.start()] + correct[am.end():]
javadoc = ("/**\n * " + "\n * ".join(doc.splitlines()) + ("\n *\n * @author " + author if author else "")
+ "\n */\n") if doc else ""
code = re.sub(r"(public class " + up + r"\b)", javadoc.replace("\\", "\\\\") + r"\1", correct, count=1)
t = strip_pkg(t).replace("java_programs.", "").replace(f"class {up}_TEST", f"class {up}Test")
if "QuixFixOracleHelper" in t:
helper = strip_pkg((qb / "java_testcases" / "junit" / "QuixFixOracleHelper.java").read_text())
helper = helper.replace("public class QuixFixOracleHelper", "class QuixFixOracleHelper")
t = t.replace("java_testcases.junit.QuixFixOracleHelper", "QuixFixOracleHelper") + "\n" + helper
t5 = (t.replace("@org.junit.Test(timeout = 3000)",
"@org.junit.jupiter.api.Test @org.junit.jupiter.api.Timeout(value = 3000, unit = "
"java.util.concurrent.TimeUnit.MILLISECONDS)")
.replace("org.junit.Assert.", "org.junit.jupiter.api.Assertions."))
return {"code": code, "tests": t, "tests5": t5, "patch": patch, "ext": "java", "file": f"{up}.java"}
def validate(lang: str, it: dict) -> tuple[bool, str]:
runner = get_runner(lang)
junit4 = lang == "java" and runner.framework() == "junit4"
tests = it["tests"] if (lang != "java" or junit4) else it["tests5"]
base = runner.run(it["code"], tests, "unit", None, with_coverage=False, timeout=60)
if not base.all_green:
return False, f"referans yeşil değil ({base.passed}/{base.total}, derlendi={base.compiled})"
buggy = it["code"].replace(it["patch"][0], it["patch"][1], 1)
r = runner.run(buggy, tests, "unit", None, with_coverage=False, timeout=60)
caught = (not r.compiled) or r.timed_out or bool(r.failing_names())
return (True, "ok") if caught else (False, "özgün hata referans testlerce yakalanmıyor")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("quixbugs", type=Path)
ap.add_argument("--per-language", type=int, default=10)
ap.add_argument("--seed", type=int, default=2026)
args = ap.parse_args()
qb = args.quixbugs
names = sorted(p.stem for p in (qb / "correct_python_programs").glob("*.py")
if not p.stem.endswith("_test") and p.stem != "node")
py_ok, java_ok, report = {}, {}, []
for n in names:
p = python_item(qb, n)
if p:
ok, why = validate("python", p)
report.append(("python", n, why))
if ok:
py_ok[n] = p
doc = docstring((qb / "python_programs" / f"{n}.py").read_text())[1]
j = java_item(qb, n, doc)
if j:
ok, why = validate("java", j)
report.append(("java", n, why))
if ok:
java_ok[n] = j
print(f" {n:<28} python={'✓' if n in py_ok else '·'} java={'✓' if n in java_ok else '·'}", flush=True)
# Farklı programları iki dile paylaştır (aynı algoritma iki kez sayılmasın)
rng = random.Random(args.seed)
only_java = [n for n in java_ok if n not in py_ok]
both = [n for n in java_ok if n in py_ok]
rng.shuffle(only_java)
rng.shuffle(both)
java_sel = (only_java + both)[:args.per_language]
py_pool = [n for n in py_ok if n not in java_sel]
rng.shuffle(py_pool)
py_sel = py_pool[:args.per_language]
if OUT.exists():
shutil.rmtree(OUT)
for d in ("python", "java", "reference"):
(OUT / d).mkdir(parents=True)
lic = (qb / "LICENSE").read_text()
(OUT / "LICENSE-QuixBugs.txt").write_text(lic)
(OUT / "README.md").write_text(
"# QuixBugs alt kümesi (bağımsız bölüm)\n\nKaynak: https://github.com/jkoppel/QuixBugs (MIT, © 2017-2019 "
"James Koppel). Programlar ve hatalar QuixBugs yazarlarına aittir. Dönüşümler: doğru sürüm test edilecek "
"kod olarak alındı; Python açıklama dizgisi Java sürümüne Javadoc olarak eklendi; QuixBugs test vakaları "
"pytest / JUnit biçimine çevrildi. Üretim: `scripts/import_quixbugs.py`.\n")
manifest_path = ROOT / "benchmarks" / "manifest.json"
manifest = [m for m in json.loads(manifest_path.read_text(encoding="utf-8")) if m.get("split") != "bagimsiz"]
for lang, sel, pool in (("python", py_sel, py_ok), ("java", java_sel, java_ok)):
for n in sorted(sel):
it = pool[n]
(OUT / lang / it["file"]).write_text(it["code"])
short = "py" if lang == "python" else "java"
ref = OUT / "reference" / f"qb-{short}-{n}"
entry = {"id": f"qb-{short}-{n}", "title": f"QuixBugs: {n} ({'Python' if lang == 'python' else 'Java'})",
"language": lang, "kind": "unit", "file": f"quixbugs/{lang}/{it['file']}",
"description": "QuixBugs programı; tek satırlık özgün hata", "split": "bagimsiz",
"source": SOURCE,
"seeded_bugs": [{"description": "QuixBugs özgün hatası", "from": it["patch"][0],
"to": it["patch"][1]}]}
if lang == "python":
ref.with_suffix(".py").write_text(it["tests"])
entry["reference_tests"] = f"quixbugs/reference/{ref.name}.py"
else:
ref.with_suffix(".junit5.java").write_text(it["tests5"])
ref.with_suffix(".junit4.java").write_text(it["tests"])
entry["reference_tests"] = f"quixbugs/reference/{ref.name}.junit5.java"
entry["reference_tests_junit4"] = f"quixbugs/reference/{ref.name}.junit4.java"
manifest.append(entry)
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
(OUT / "import_report.json").write_text(json.dumps(
{"seed": args.seed, "python_valid": sorted(py_ok), "java_valid": sorted(java_ok),
"python_selected": sorted(py_sel), "java_selected": sorted(java_sel),
"checks": [{"language": a, "program": b, "result": c} for a, b, c in report]}, ensure_ascii=False, indent=1))
print(f"\nGeçerli: Python {len(py_ok)}, Java {len(java_ok)}. Seçilen: Python {sorted(py_sel)}; Java {sorted(java_sel)}")
if __name__ == "__main__":
main()
|