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()