File size: 5,342 Bytes
c4f3819
 
 
 
 
 
 
 
4c69128
c4f3819
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c69128
 
 
 
 
 
 
 
 
c4f3819
 
 
 
 
 
 
 
 
 
 
 
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
from pathlib import Path
import shutil
import sys

from analyzer import analyze
from agent import forge_legacy_repo
from generator import capture, generate_suite, write_suite
from inject import reset_sample, run_injected_suite
from model_suggest import _parse_tuples
from mutator import MUTATIONS, mutation_score
from patch import make_pr_patch
from runner import run_pytest
from samples.legacy_repo.pricing import apply_discount, with_tax


ROOT = Path(__file__).resolve().parents[1]
LEGACY_ROOT = ROOT / "samples" / "legacy_repo"


def test_analyzer_discovers_expected_public_functions():
    analysis = analyze(LEGACY_ROOT, package="legacy_repo")
    discovered = {(fn.module, fn.qualname, fn.arity) for fn in analysis.functions}

    assert discovered == {
        ("legacy_repo.dates", "days_between", 2),
        ("legacy_repo.dates", "is_weekend", 1),
        ("legacy_repo.invoice", "invoice_total", 1),
        ("legacy_repo.invoice", "line_total", 2),
        ("legacy_repo.pricing", "apply_discount", 2),
        ("legacy_repo.pricing", "bulk_price", 2),
        ("legacy_repo.pricing", "with_tax", 2),
        ("legacy_repo.slugify", "slugify", 1),
        ("legacy_repo.slugify", "truncate", 2),
    }


def test_analyzer_keeps_hints_docstrings_and_source():
    analysis = analyze(LEGACY_ROOT, package="legacy_repo")
    fn = next(item for item in analysis.functions if item.qualname == "apply_discount")

    assert fn.type_hints == {"price": "float", "pct": "float"}
    assert fn.return_hint == "float"
    assert "percentage discount" in fn.docstring
    assert "def apply_discount" in fn.source


def test_capture_records_return_and_exception():
    sys.path.insert(0, str((ROOT / "samples").resolve()))
    analysis = analyze(LEGACY_ROOT, package="legacy_repo")
    discount = next(item for item in analysis.functions if item.qualname == "apply_discount")
    tax = next(item for item in analysis.functions if item.qualname == "with_tax")

    assert apply_discount(100, 10) == 90.0
    returned = capture(discount, [(100.0, 10.0)])
    raised = capture(tax, [(100.0, -1.0)])

    assert returned[0].value_repr == "90.0"
    assert raised[0].exception_type == "ValueError"


def test_deterministic_generated_suite_is_green(tmp_path):
    analysis = analyze(LEGACY_ROOT, package="legacy_repo")
    suite = generate_suite(analysis)
    workdir = tmp_path / "work"
    shutil.copytree(ROOT / "samples", workdir / "samples")
    write_suite(suite, workdir)

    result = run_pytest(workdir, package_parent=workdir / "samples")

    assert result.ok, result.stdout + result.stderr
    assert result.passed == suite.assertion_count
    assert result.coverage is None or result.coverage >= 0


def test_runner_parses_known_good_and_bad(tmp_path):
    good = tmp_path / "good"
    good.mkdir()
    (good / "tests").mkdir()
    (good / "tests" / "test_ok.py").write_text("def test_ok():\n    assert 1 == 1\n", encoding="utf-8")
    good_result = run_pytest(good, with_coverage=False)

    bad = tmp_path / "bad"
    bad.mkdir()
    (bad / "tests").mkdir()
    (bad / "tests" / "test_bad.py").write_text("def test_bad():\n    assert 1 == 2\n", encoding="utf-8")
    bad_result = run_pytest(bad, with_coverage=False)

    assert good_result.ok
    assert good_result.passed == 1
    assert not bad_result.ok
    assert bad_result.failed == 1


def test_mutation_score_is_deterministic_and_kills_known_mutant(tmp_path):
    analysis = analyze(LEGACY_ROOT, package="legacy_repo")
    suite = generate_suite(analysis)
    score = mutation_score(ROOT / "samples", suite, tmp_path / "mutants")

    assert score.total == len(MUTATIONS)
    assert score.killed > 0
    known = next(
        result for result in score.results if result.mutation.id == "bulk_multiply_to_divide"
    )
    assert known.killed


def test_patch_export_contains_generated_tests():
    analysis = analyze(LEGACY_ROOT, package="legacy_repo")
    suite = generate_suite(analysis)
    patch = make_pr_patch(suite)

    assert "b/tests/test_legacy_repo_pricing_apply_discount.py" in patch
    assert "pytest tests -q" in patch


def test_forge_pipeline_produces_green_suite_and_patch():
    one_mutant = (next(item for item in MUTATIONS if item.id == "bulk_threshold_ge_to_gt"),)
    artifacts = forge_legacy_repo(max_cases_per_function=2, mutations=one_mutant)

    assert artifacts.green.ok, artifacts.green.stdout + artifacts.green.stderr
    assert artifacts.suite.assertion_count > 0
    assert artifacts.mutation.killed > 0
    assert artifacts.patch_path.exists()


def test_model_suggest_parses_only_arity_matched_tuples():
    text = "Here you go: [(0, 'x'), (-1, ''), (1, 2, 3)]"

    assert _parse_tuples(text, arity=2, limit=5) == [(0, "x"), (-1, "")]
    assert _parse_tuples("[(0, 1, 2)]", arity=2, limit=2) == []
    assert _parse_tuples("no list here", arity=2, limit=2) == []
    assert _parse_tuples("[5, -1, 0]", arity=1, limit=2) == [(5,), (-1,)]


def test_inject_regression_causes_exactly_one_failure(tmp_path):
    analysis = analyze(LEGACY_ROOT, package="legacy_repo")
    suite = generate_suite(analysis)
    run_dir = tmp_path / "inject"
    run_dir.mkdir()
    reset_sample(ROOT / "samples", run_dir)
    write_suite(suite, run_dir)

    result = run_injected_suite(run_dir)

    assert not result.run.ok
    assert result.run.failed == 1