@jesdi/skills 0.0.50 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/test-prune/SKILL.md +156 -0
- package/skills/test-prune/prune.py +202 -0
- package/skills/test-prune/prune_repo.py +319 -0
- package/skills/test-prune/prune_review.py +363 -0
- package/skills/test-prune/prune_runs.py +240 -0
- package/skills/test-prune/prune_source.py +237 -0
- package/skills/test-prune/prune_strengthen.py +301 -0
- package/skills/test-prune/tests/conftest.py +15 -0
- package/skills/test-prune/tests/prune_harness.py +205 -0
- package/skills/test-prune/tests/test_prune_guards.py +871 -0
- package/skills/test-prune/tests/test_prune_inputs.py +97 -0
- package/skills/test-prune/tests/test_prune_review.py +113 -0
- package/skills/test-prune/tests/test_prune_source.py +84 -0
- package/skills/test-prune/tests/test_prune_strengthen.py +183 -0
- package/skills/test-prune/tests/test_prune_unattended.py +225 -0
- package/skills/test-prune/tests/testdata/gh +16 -0
- package/skills/test-prune/tests/testdata/repo/Makefile +25 -0
- package/skills/test-prune/tests/testdata/repo/backend/mutation_baseline.json +16 -0
- package/skills/test-prune/tests/testdata/repo/backend/services/concentration.py +2 -0
- package/skills/test-prune/tests/testdata/repo/backend/services/rebalancer.py +11 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/conftest.py +6 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/test_concentration.py +73 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/test_rebalancer.py +77 -0
- package/skills/test-prune/tests/testdata/repo/fake_engine.py +289 -0
- package/skills/test-prune/tests/testdata/repo/kill_map.json +131 -0
- package/skills-manifest.json +7 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Agent-input guards and proof rules, checked in-process: each rejection names its id."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from prune_repo import BadInput, Fail, parse_ratchet, read_agent_json
|
|
7
|
+
from prune_review import candidate_problems, group_problems
|
|
8
|
+
from prune_strengthen import changed_tests, commit_problems, item_proof, reopened
|
|
9
|
+
|
|
10
|
+
TESTS = {"tests/t.py::a", "tests/t.py::b"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.parametrize("entry, problem", [
|
|
14
|
+
({"verdict": "drop", "reason": "r"}, "verdict 'drop'"),
|
|
15
|
+
({"verdict": "delete", "into": "tests/t.py::a", "reason": "r"}, "not another collected test id"),
|
|
16
|
+
({"verdict": "strengthen", "assertion": " ", "reason": "r"}, "names no assertion"),
|
|
17
|
+
({"verdict": "delete", "into": "tests/t.py::b"}, "no reason"),
|
|
18
|
+
])
|
|
19
|
+
def test_candidate_problems_name_the_candidate(entry, problem):
|
|
20
|
+
(message,) = candidate_problems("tests/t.py::a", entry, TESTS)
|
|
21
|
+
assert message.startswith("candidate tests/t.py::a:") and problem in message
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_a_kept_candidate_needs_no_reason():
|
|
25
|
+
assert candidate_problems("tests/t.py::a", {"verdict": "keep"}, TESTS) == []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
GROUP = {"group": "m.f", "mutant_ids": ["x_f__mutmut_1", "x_f__mutmut_2"]}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize("entry, problem", [
|
|
32
|
+
({"verdict": "delete", "reason": "r"}, "verdict 'delete'"),
|
|
33
|
+
({"verdict": "strengthen", "mutant_ids": [], "reason": "r"}, "non-empty list"),
|
|
34
|
+
({"verdict": "equivalent", "mutant_ids": ["x_f__mutmut_9"], "reason": "r"}, "x_f__mutmut_9 is not one"),
|
|
35
|
+
({"verdict": "strengthen"}, "no reason"),
|
|
36
|
+
])
|
|
37
|
+
def test_group_problems_name_the_group(entry, problem):
|
|
38
|
+
(message,) = group_problems(GROUP, entry)
|
|
39
|
+
assert message.startswith("weak spot m.f:") and problem in message
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize("content", ["not json", "[]", '{"reviews": {}}', '{"reviews": [1]}'])
|
|
43
|
+
def test_read_agent_json_rejects_malformed_files(tmp_path, content):
|
|
44
|
+
path = tmp_path / "reviews.json"
|
|
45
|
+
path.write_text(content)
|
|
46
|
+
with pytest.raises(BadInput, match="reviews"):
|
|
47
|
+
read_agent_json(str(path), "reviews")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_read_agent_json_names_a_missing_file(tmp_path):
|
|
51
|
+
with pytest.raises(BadInput, match="cannot read"):
|
|
52
|
+
read_agent_json(str(tmp_path / "nope.json"), "reviews")
|
|
53
|
+
(tmp_path / "ok.json").write_text(json.dumps({"reviews": [{"id": "a"}]}))
|
|
54
|
+
assert read_agent_json(str(tmp_path / "ok.json"), "reviews") == [{"id": "a"}]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_commit_problems_need_one_known_trailer_and_test_paths_only():
|
|
58
|
+
assert commit_problems("a" * 40, ["m.f"], ["backend/tests/t.py"], {"m.f"}) == []
|
|
59
|
+
trailer, outside = commit_problems("a" * 40, ["m.f", "m.g"], ["backend/services/x.py"], {"m.f"})
|
|
60
|
+
assert "exactly one" in trailer and "backend/services/x.py" in outside
|
|
61
|
+
assert commit_problems("a" * 40, [], [], {"m.f"})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_item_proof_needs_survived_on_base_and_killed_on_head_per_id():
|
|
65
|
+
item = {"module": "s.py", "mutant_ids": ["m1", "m2"]}
|
|
66
|
+
base = {"s.py": {"m1": 0, "m2": 33}}
|
|
67
|
+
|
|
68
|
+
assert item_proof(base, {"s.py": {"m1": 1, "m2": 3}}, item, True)["status"] == "proven"
|
|
69
|
+
assert item_proof(base, {"s.py": {"m1": 1, "m2": 0}}, item, True)["status"] == "unproven"
|
|
70
|
+
assert item_proof(base, {"s.py": {"m1": 1, "m2": 3}}, item, False)["status"] == "unproven"
|
|
71
|
+
assert item_proof({"s.py": {"m1": 1, "m2": 0}}, {"s.py": {"m1": 1, "m2": 1}}, item, True)["status"] == "unproven"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_changed_tests_lists_new_and_edited_tests():
|
|
75
|
+
before = "def test_a():\n assert 1\n\n\ndef test_b():\n assert 2\n"
|
|
76
|
+
after = "def test_a():\n assert 1\n\n\ndef test_b():\n assert 3\n\n\nclass TestC:\n def test_c(self):\n pass\n"
|
|
77
|
+
|
|
78
|
+
assert changed_tests(before, after, "backend/tests/t.py") == ["tests/t.py::test_b", "tests/t.py::TestC::test_c"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_a_ratchet_failure_without_survivor_counts_is_operational():
|
|
82
|
+
with pytest.raises(Fail, match="without a survivor count"):
|
|
83
|
+
parse_ratchet(" ✗ services/x.py: no mutmut results, run 'make mutation'\n", 1)
|
|
84
|
+
ratchet = parse_ratchet(" ✗ services/x.py: 23 survivors > baseline 20 (score 1.00)\n", 1)
|
|
85
|
+
assert ratchet["failed"] == ["services/x.py"] and ratchet["modules"]["services/x.py"] == [23, 20]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_an_item_without_mutant_ids_is_never_proven():
|
|
89
|
+
assert item_proof({}, {}, {"module": None, "mutant_ids": []}, True)["status"] == "unproven"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_commits_after_an_item_closed_are_reopenings():
|
|
93
|
+
history = [("a" * 40, ["m.f"], []), ("b" * 40, ["m.f"], []), ("c" * 40, ["m.f"], []), ("d" * 40, ["m.g"], [])]
|
|
94
|
+
|
|
95
|
+
(problem,) = reopened(history, {"m.f": "b" * 40})
|
|
96
|
+
assert problem.startswith("cccccccccccc: m.f was closed")
|
|
97
|
+
assert reopened(history, {"m.g": "start"}) and not reopened(history, {})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Fixer/reviewer hand-off, interactive mode and input guards (ticket 21)."""
|
|
2
|
+
from prune_harness import REFERENCE, R, cand, case
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_reviewer_rejects_zero_weight_into_small_weight_with_the_boundary_reason(target):
|
|
6
|
+
zero, small = R + "test_zero_weight", R + "test_small_weight"
|
|
7
|
+
reason = "test_zero_weight asserts the zero-weight boundary (a full buy at 0.0); test_small_weight never checks 0"
|
|
8
|
+
|
|
9
|
+
out = target.prepare([cand(zero, small)], unattended=False, deletions={zero: small},
|
|
10
|
+
accept=(), reasons={zero: reason}).stdout
|
|
11
|
+
|
|
12
|
+
# The fixer saw the candidate's and the container's source and assertions.
|
|
13
|
+
(fixer_cand,) = target.read_state("fixer-input.json")["candidates"]
|
|
14
|
+
assert fixer_cand["test_id"] == zero and fixer_cand["contained_by"] == [small]
|
|
15
|
+
assert "def test_zero_weight(targets):" in fixer_cand["source"]
|
|
16
|
+
assert [c["test_id"] for c in fixer_cand["containers"]] == [small]
|
|
17
|
+
|
|
18
|
+
# The reviewer sees both tests' assertions, not only coverage.
|
|
19
|
+
(proposal,) = [p for p in target.read_state("review-input.json")["proposals"] if p["id"] == zero]
|
|
20
|
+
assert proposal["kind"] == "delete"
|
|
21
|
+
assert proposal["candidate"]["test_id"] == zero and proposal["into"]["test_id"] == small
|
|
22
|
+
assert any('trades["AAA"] == pytest.approx(0.6)' in a for a in proposal["candidate"]["assertions"])
|
|
23
|
+
assert any('assert trades["AAA"] > 0' in a for a in proposal["into"]["assertions"])
|
|
24
|
+
assert any('assert set(trades) == {"AAA", "BBB"}' in a for a in proposal["into"]["assertions"])
|
|
25
|
+
assert "def test_small_weight(targets):" in proposal["into"]["source"]
|
|
26
|
+
|
|
27
|
+
batch = target.read_state("batch.json")
|
|
28
|
+
assert zero not in [d["test_id"] for d in batch["deletions"]]
|
|
29
|
+
assert {"id": zero, "kind": "delete", "reason": reason} in batch["rejected"]
|
|
30
|
+
assert zero in out and reason in out
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_interactive_twelve_deletions_and_three_strengthen_edit_nothing_before_approval(target):
|
|
34
|
+
tests_dir = target.root / "backend" / "tests"
|
|
35
|
+
before = {p.name: p.read_bytes() for p in tests_dir.iterdir() if p.is_file()}
|
|
36
|
+
deletions = {case(i): REFERENCE for i in range(1, 13)}
|
|
37
|
+
strengthen = {case(i): "assert herfindahl([0.25] * 4) == pytest.approx(0.25)" for i in range(13, 16)}
|
|
38
|
+
|
|
39
|
+
target.prepare([cand(case(i)) for i in range(1, 16)], unattended=False,
|
|
40
|
+
accept=[*deletions, *strengthen], deletions=deletions, strengthen=strengthen)
|
|
41
|
+
|
|
42
|
+
batch = target.read_state("batch.json")
|
|
43
|
+
assert sorted(d["test_id"] for d in batch["deletions"]) == sorted(deletions)
|
|
44
|
+
assert sorted(s["id"] for s in batch["strengthen"] if s["kind"] == "candidate") == sorted(strengthen)
|
|
45
|
+
assert target.git("status", "--porcelain") == ""
|
|
46
|
+
assert {p.name: p.read_bytes() for p in tests_dir.iterdir() if p.is_file()} == before
|
|
47
|
+
assert target.git("rev-parse", "HEAD").strip() == target.base
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_interactive_apply_commits_approved_deletions_and_restores_the_culprit(target):
|
|
51
|
+
culprit = R + "test_sell_only_rebalance"
|
|
52
|
+
deletions = {case(1): REFERENCE, case(2): REFERENCE, culprit: R + "test_hold_when_on_target"}
|
|
53
|
+
target.prepare([cand(t, into) for t, into in deletions.items()], unattended=False,
|
|
54
|
+
accept=list(deletions), deletions=deletions)
|
|
55
|
+
|
|
56
|
+
out = target.ok("apply").stdout
|
|
57
|
+
|
|
58
|
+
assert target.git("symbolic-ref", "--short", "HEAD").strip() == "main"
|
|
59
|
+
assert target.git("log", "-1", "--format=%s").strip() == "test: delete 2 redundant tests"
|
|
60
|
+
assert target.git("rev-parse", "HEAD~1").strip() == target.base
|
|
61
|
+
head_ids = target.ids_at("HEAD")
|
|
62
|
+
assert case(1) not in head_ids and case(2) not in head_ids and culprit in head_ids
|
|
63
|
+
assert target.git("status", "--porcelain") == ""
|
|
64
|
+
assert culprit in out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_apply_in_an_unattended_state_exits_2(target):
|
|
68
|
+
target.prepare([cand(case(1))], accept=[case(1)], deletions={case(1): REFERENCE})
|
|
69
|
+
|
|
70
|
+
proc = target.run("apply")
|
|
71
|
+
|
|
72
|
+
assert proc.returncode == 2, proc.stdout + proc.stderr
|
|
73
|
+
assert target.git("rev-parse", "HEAD").strip() == target.base
|
|
74
|
+
assert target.git("status", "--porcelain") == ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_prune_in_an_interactive_state_exits_2(target):
|
|
78
|
+
target.prepare([cand(case(1))], unattended=False, accept=[case(1)], deletions={case(1): REFERENCE})
|
|
79
|
+
|
|
80
|
+
proc = target.run("prune")
|
|
81
|
+
|
|
82
|
+
assert proc.returncode == 2, proc.stdout + proc.stderr
|
|
83
|
+
assert target.remote_branches() == {"main"}
|
|
84
|
+
assert target.log("gh") == []
|
|
85
|
+
assert target.git("status", "--porcelain") == ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_collect_on_a_dirty_tracked_tree_exits_2(target):
|
|
89
|
+
path = target.root / "backend" / "tests" / "test_rebalancer.py"
|
|
90
|
+
path.write_text(path.read_text() + "\n# local edit\n")
|
|
91
|
+
target.set_candidates([cand(case(1))])
|
|
92
|
+
|
|
93
|
+
proc = target.run("collect")
|
|
94
|
+
|
|
95
|
+
assert proc.returncode == 2, proc.stdout + proc.stderr
|
|
96
|
+
assert "test_rebalancer.py" in proc.stdout + proc.stderr
|
|
97
|
+
assert target.log("mutation") == []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_invalid_proposals_exit_2_naming_the_ids(target):
|
|
101
|
+
target.set_candidates([cand(case(1)), cand(case(2))])
|
|
102
|
+
target.ok("collect")
|
|
103
|
+
|
|
104
|
+
missing = target.proposals(deletions={case(1): REFERENCE})
|
|
105
|
+
missing["candidates"] = [c for c in missing["candidates"] if c["test_id"] != case(2)]
|
|
106
|
+
proc = target.review_input(missing)
|
|
107
|
+
assert proc.returncode == 2, proc.stdout + proc.stderr
|
|
108
|
+
assert case(2) in proc.stdout + proc.stderr
|
|
109
|
+
|
|
110
|
+
nope = "tests/test_concentration.py::test_nope"
|
|
111
|
+
proc = target.review_input(target.proposals(deletions={case(1): nope}))
|
|
112
|
+
assert proc.returncode == 2, proc.stdout + proc.stderr
|
|
113
|
+
assert nope in proc.stdout + proc.stderr
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Deletion-shape edges the fixture repo does not reach, checked in-process (prune_source)."""
|
|
2
|
+
from prune_source import class_refusals, delete_tests, describe, undeletable
|
|
3
|
+
from prune_source import test_sources as sources
|
|
4
|
+
|
|
5
|
+
EDGES = '''"""Edges."""
|
|
6
|
+
import os
|
|
7
|
+
import json as j
|
|
8
|
+
from typing import Any
|
|
9
|
+
from helpers import make_thing, other
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestFirst:
|
|
13
|
+
def test_first(self):
|
|
14
|
+
assert os.sep
|
|
15
|
+
|
|
16
|
+
def test_second(self):
|
|
17
|
+
assert True
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_uses_param(other):
|
|
21
|
+
assert other
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_anno(x: "Any"):
|
|
25
|
+
assert make_thing()
|
|
26
|
+
assert j.dumps({}) == "{}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_kept():
|
|
30
|
+
with pytest.raises(ValueError):
|
|
31
|
+
int("x")
|
|
32
|
+
'''
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_first_class_member_takes_the_blank_lines_after_it_and_pinned_imports_stay():
|
|
36
|
+
out = delete_tests(EDGES, {(("TestFirst",), "test_first"), ((), "test_uses_param"), ((), "test_anno")})
|
|
37
|
+
|
|
38
|
+
assert "class TestFirst:\n def test_second(self):" in out
|
|
39
|
+
assert "import os\n" not in out and "import json as j\n" not in out # read only by deleted tests
|
|
40
|
+
assert "from typing import Any\n" in out # a string carries Any
|
|
41
|
+
assert "from helpers import make_thing, other\n" in out # a parameter carries other
|
|
42
|
+
assert out.endswith(' int("x")\n')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_undeletable_names_what_the_prune_gate_would_refuse():
|
|
46
|
+
decorated = "import pytest\n\n\nclass TestX:\n @pytest.mark.slow\n def test_a(self):\n assert 1\n"
|
|
47
|
+
named = "def test_a():\n assert 1\n\n\nALIAS = test_a\n"
|
|
48
|
+
|
|
49
|
+
assert "decorated first member" in undeletable(decorated, "tests/t.py::TestX::test_a")
|
|
50
|
+
assert "reads or spells the name test_a" in undeletable(named, "tests/t.py::test_a")
|
|
51
|
+
assert "cannot be deleted whole" in undeletable(named, "tests/t.py::test_missing")
|
|
52
|
+
assert undeletable(EDGES, "tests/t.py::TestFirst::test_second") is None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_describe_lists_raises_blocks_and_misses_unknown_tests():
|
|
56
|
+
found = describe(EDGES, "tests/t.py::test_kept")
|
|
57
|
+
|
|
58
|
+
assert found["assertions"] == ['with pytest.raises(ValueError):\n int("x")']
|
|
59
|
+
assert describe(EDGES, "tests/t.py::TestNope::test_kept") is None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_a_test_in_a_class_named_elsewhere_is_undeletable():
|
|
63
|
+
subclassed = "class TestBase:\n def test_a(self):\n assert 1\n\n def test_b(self):\n assert 2\n\n\nclass TestChild(TestBase):\n def test_c(self):\n assert 3\n"
|
|
64
|
+
spelled = "class TestA:\n def test_a(self):\n assert 1\n\n\ndef test_b():\n assert 'TestA'\n"
|
|
65
|
+
|
|
66
|
+
assert "class TestBase is subclassed" in undeletable(subclassed, "tests/t.py::TestBase::test_a") # partial
|
|
67
|
+
assert "class TestA is subclassed or named" in undeletable(spelled, "tests/t.py::TestA::test_a")
|
|
68
|
+
assert undeletable(subclassed, "tests/t.py::TestChild::test_c") is None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_class_refusals_keep_an_enclosing_class_non_empty():
|
|
72
|
+
nested = "class TestOuter:\n class TestInner:\n def test_a(self):\n assert 1\n"
|
|
73
|
+
documented = 'class TestOuter:\n """Doc."""\n\n class TestInner:\n def test_a(self):\n assert 1\n'
|
|
74
|
+
inner = "tests/t.py::TestOuter::TestInner::test_a"
|
|
75
|
+
|
|
76
|
+
assert "leaves class TestOuter empty" in class_refusals(nested, [inner])[inner]
|
|
77
|
+
assert class_refusals(documented, [inner]) == {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_a_new_parametrize_case_changes_the_test_source():
|
|
81
|
+
before = "@pytest.mark.parametrize('x', [1])\ndef test_x(x):\n assert x\n"
|
|
82
|
+
after = before.replace("[1]", "[1, 2]")
|
|
83
|
+
|
|
84
|
+
assert sources(before)["test_x"] != sources(after)["test_x"]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Strengthen PR, equivalent proposals and cold mutation runs (ticket 21)."""
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from prune_harness import PRUNE_BRANCH, REFERENCE, STRENGTHEN_BRANCH, R, arg, cand, case
|
|
6
|
+
|
|
7
|
+
COMPUTE = "rebalancer.compute_trades"
|
|
8
|
+
DRIFT = "rebalancer.apply_drift"
|
|
9
|
+
HERFINDAHL = "concentration.herfindahl"
|
|
10
|
+
IDS_7_9 = ["x_compute_trades__mutmut_7", "x_compute_trades__mutmut_9"]
|
|
11
|
+
EQUIVALENT_ID = "x_herfindahl__mutmut_9"
|
|
12
|
+
EQUIVALENT_REASON = "the <= mutant only differs on an empty weight list, which herfindahl never receives"
|
|
13
|
+
|
|
14
|
+
COMPUTE_TEST = '''
|
|
15
|
+
def test_compute_trades_rounding(targets):
|
|
16
|
+
trades = compute_trades({"AAA": 0.333, "BBB": 0.667}, targets)
|
|
17
|
+
assert round(trades["AAA"], 3) == 0.267
|
|
18
|
+
'''
|
|
19
|
+
DRIFT_TEST = '''
|
|
20
|
+
def test_apply_drift_new():
|
|
21
|
+
assert apply_drift(-0.5) == 0
|
|
22
|
+
''' # kills x_apply_drift__mutmut_2 only, not _4
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def strengthen_group(ids, reason="fixer: no test pins this"):
|
|
26
|
+
return {"verdict": "strengthen", "mutant_ids": ids, "test": "tests/test_rebalancer.py", "reason": reason}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def codes_for(proof, mutant_id):
|
|
30
|
+
"""(base code, head code) of mutant_id anywhere in strengthen-proof.json."""
|
|
31
|
+
found = []
|
|
32
|
+
|
|
33
|
+
def walk(node):
|
|
34
|
+
if isinstance(node, dict):
|
|
35
|
+
if mutant_id in node and isinstance(node[mutant_id], dict):
|
|
36
|
+
found.append(node[mutant_id])
|
|
37
|
+
if mutant_id in (node.get("mutant_id"), node.get("id")):
|
|
38
|
+
found.append(node)
|
|
39
|
+
for value in node.values():
|
|
40
|
+
walk(value)
|
|
41
|
+
elif isinstance(node, list):
|
|
42
|
+
for value in node:
|
|
43
|
+
walk(value)
|
|
44
|
+
|
|
45
|
+
walk(proof)
|
|
46
|
+
assert found, f"{mutant_id} not in strengthen-proof.json: {proof}"
|
|
47
|
+
entry = found[0]
|
|
48
|
+
base = next(v for k, v in entry.items() if k.startswith("base"))
|
|
49
|
+
head = next(v for k, v in entry.items() if k.startswith("head"))
|
|
50
|
+
return base, head
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def baseline_at(target, ref):
|
|
54
|
+
return json.loads(target.show(ref, "backend/mutation_baseline.json"))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def strengthen(target, agent_tests, finish_accept, env=None, before=lambda: None):
|
|
58
|
+
"""strengthen-start → agent commits → strengthen-verify → strengthen-finish; returns the finish run."""
|
|
59
|
+
before()
|
|
60
|
+
target.ok("strengthen-start", env=env)
|
|
61
|
+
assert target.git("rev-parse", "HEAD").strip() == target.base
|
|
62
|
+
for item, source in agent_tests:
|
|
63
|
+
target.commit_test(item, source)
|
|
64
|
+
before()
|
|
65
|
+
target.ok("strengthen-verify", env=env)
|
|
66
|
+
before()
|
|
67
|
+
return target.run("strengthen-finish", "--reviews",
|
|
68
|
+
str(target.reviews(finish_accept, ids=list(finish_accept))), env=env)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def strengthen_pr(target):
|
|
72
|
+
(create,) = [c for c in target.pr_creates() if arg(c["argv"], "--head") == STRENGTHEN_BRANCH]
|
|
73
|
+
assert arg(create["argv"], "--base") == "main"
|
|
74
|
+
return create["body"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_strengthen_pr_names_ids_7_and_9_killed_proves_them_surviving_before_and_lowers_the_baseline_by_2(target):
|
|
78
|
+
target.prepare([cand(case(1))], accept=[case(1), COMPUTE], deletions={case(1): REFERENCE},
|
|
79
|
+
groups={COMPUTE: strengthen_group(IDS_7_9)})
|
|
80
|
+
target.ok("prune")
|
|
81
|
+
prune_head = target.remote_sha(PRUNE_BRANCH)
|
|
82
|
+
|
|
83
|
+
finish = strengthen(target, [(COMPUTE, COMPUTE_TEST)], [COMPUTE])
|
|
84
|
+
|
|
85
|
+
assert finish.returncode == 0, finish.stdout + finish.stderr
|
|
86
|
+
assert all(i in (target.state / "strengthen-input.json").read_text() for i in IDS_7_9)
|
|
87
|
+
proof = target.read_state("strengthen-proof.json")
|
|
88
|
+
for mutant_id in IDS_7_9:
|
|
89
|
+
base_code, head_code = codes_for(proof, mutant_id)
|
|
90
|
+
assert base_code in (0, 5, 33) and head_code in (1, 3), (mutant_id, base_code, head_code)
|
|
91
|
+
|
|
92
|
+
head = target.remote_sha(STRENGTHEN_BRANCH)
|
|
93
|
+
assert baseline_at(target, target.base)["services/rebalancer.py"]["survivors"] == 20
|
|
94
|
+
assert baseline_at(target, head)["services/rebalancer.py"]["survivors"] == 18
|
|
95
|
+
assert "def test_compute_trades_rounding(targets):" in target.show(head, "backend/tests/test_rebalancer.py")
|
|
96
|
+
# Branched from the base commit, independent of the prune branch.
|
|
97
|
+
assert target.git("merge-base", head, prune_head).strip() == target.base
|
|
98
|
+
for path in target.git("diff", "--name-only", target.base, head).split():
|
|
99
|
+
assert path.startswith("backend/tests/") or path == "backend/mutation_baseline.json", path
|
|
100
|
+
|
|
101
|
+
body = strengthen_pr(target)
|
|
102
|
+
assert "test_compute_trades_rounding" in body
|
|
103
|
+
for mutant_id in IDS_7_9:
|
|
104
|
+
assert mutant_id in body
|
|
105
|
+
assert re.search(r"surviv", body, re.I)
|
|
106
|
+
assert re.search(r"services/rebalancer\.py\D*\b20\b\D+\b18\b", body), body
|
|
107
|
+
assert not [c for c in target.log("gh") if c["argv"][:2] == ["pr", "merge"]]
|
|
108
|
+
assert target.git("rev-parse", "main").strip() == target.base == target.remote_sha("main")
|
|
109
|
+
assert target.git("symbolic-ref", "--short", "HEAD").strip() == "main"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_strengthen_item_whose_ids_are_not_all_killed_is_reverted_and_left_out_of_the_pr(target):
|
|
113
|
+
drift_ids = ["x_apply_drift__mutmut_2", "x_apply_drift__mutmut_4"]
|
|
114
|
+
target.prepare([cand(case(1))], accept=[COMPUTE, DRIFT],
|
|
115
|
+
groups={COMPUTE: strengthen_group(IDS_7_9), DRIFT: strengthen_group(drift_ids)})
|
|
116
|
+
|
|
117
|
+
finish = strengthen(target, [(COMPUTE, COMPUTE_TEST), (DRIFT, DRIFT_TEST)], [COMPUTE])
|
|
118
|
+
|
|
119
|
+
assert finish.returncode == 0, finish.stdout + finish.stderr
|
|
120
|
+
head = target.remote_sha(STRENGTHEN_BRANCH)
|
|
121
|
+
source = target.show(head, "backend/tests/test_rebalancer.py")
|
|
122
|
+
assert "def test_compute_trades_rounding(targets):" in source
|
|
123
|
+
assert "test_apply_drift_new" not in source
|
|
124
|
+
assert baseline_at(target, head)["services/rebalancer.py"]["survivors"] == 18
|
|
125
|
+
body = strengthen_pr(target)
|
|
126
|
+
assert "test_apply_drift_new" not in body
|
|
127
|
+
assert all(i not in body for i in drift_ids)
|
|
128
|
+
assert all(i in body for i in IDS_7_9)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_unattended_equivalent_proposals_are_listed_in_the_pr_bodies_and_never_written(target):
|
|
132
|
+
target.prepare([cand(case(1))], accept=[case(1), COMPUTE, HERFINDAHL], deletions={case(1): REFERENCE},
|
|
133
|
+
groups={COMPUTE: strengthen_group(IDS_7_9),
|
|
134
|
+
HERFINDAHL: {"verdict": "equivalent", "mutant_ids": [EQUIVALENT_ID],
|
|
135
|
+
"test": "tests/test_concentration.py", "reason": EQUIVALENT_REASON}})
|
|
136
|
+
equivalents = target.read_state("batch.json")["equivalents"]
|
|
137
|
+
assert [(e["group"], e["mutant_ids"]) for e in equivalents] == [(HERFINDAHL, [EQUIVALENT_ID])]
|
|
138
|
+
|
|
139
|
+
target.ok("prune")
|
|
140
|
+
finish = strengthen(target, [(COMPUTE, COMPUTE_TEST)], [COMPUTE])
|
|
141
|
+
|
|
142
|
+
assert finish.returncode == 0, finish.stdout + finish.stderr
|
|
143
|
+
(prune_pr,) = [c for c in target.pr_creates() if arg(c["argv"], "--head") == PRUNE_BRANCH]
|
|
144
|
+
for body in (prune_pr["body"], strengthen_pr(target)):
|
|
145
|
+
assert EQUIVALENT_ID in body and EQUIVALENT_REASON in body
|
|
146
|
+
head = target.remote_sha(STRENGTHEN_BRANCH)
|
|
147
|
+
for module, entry in baseline_at(target, head).items():
|
|
148
|
+
assert entry["equivalent"] == {}, module
|
|
149
|
+
assert (target.root / "backend" / "mutation_baseline.json").read_text() == \
|
|
150
|
+
target.show(target.base, "backend/mutation_baseline.json")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_equivalent_guard_trips_when_the_baseline_update_gains_an_equivalent_id(target):
|
|
154
|
+
target.prepare([cand(case(1))], accept=[COMPUTE], groups={COMPUTE: strengthen_group(IDS_7_9)})
|
|
155
|
+
|
|
156
|
+
finish = strengthen(target, [(COMPUTE, COMPUTE_TEST)], [COMPUTE],
|
|
157
|
+
env={"FAKE_SNEAK_EQUIVALENT": f"services/concentration.py:{EQUIVALENT_ID}"})
|
|
158
|
+
|
|
159
|
+
assert finish.returncode == 1, finish.stdout + finish.stderr
|
|
160
|
+
assert EQUIVALENT_ID in finish.stdout + finish.stderr
|
|
161
|
+
assert target.log("mutation-baseline"), "the guard must run after make mutation-baseline"
|
|
162
|
+
assert STRENGTHEN_BRANCH not in target.remote_branches()
|
|
163
|
+
assert target.pr_creates() == []
|
|
164
|
+
assert target.git("rev-parse", "main").strip() == target.base == target.remote_sha("main")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_every_post_batch_mutation_run_starts_from_an_empty_mutants_directory(target):
|
|
168
|
+
culprit = R + "test_sell_only_rebalance"
|
|
169
|
+
keep_cache = {"KEEP_CACHE": "1"}
|
|
170
|
+
deletions = {case(1): REFERENCE, culprit: R + "test_hold_when_on_target"}
|
|
171
|
+
target.prepare([cand(i, into) for i, into in deletions.items()], accept=[*deletions, COMPUTE],
|
|
172
|
+
deletions=deletions, groups={COMPUTE: strengthen_group(IDS_7_9)},
|
|
173
|
+
env=keep_cache, before=target.junk_mutants)
|
|
174
|
+
target.junk_mutants()
|
|
175
|
+
target.ok("prune", env=keep_cache)
|
|
176
|
+
finish = strengthen(target, [(COMPUTE, COMPUTE_TEST)], [COMPUTE], env=keep_cache, before=target.junk_mutants)
|
|
177
|
+
assert finish.returncode == 0, finish.stdout + finish.stderr
|
|
178
|
+
|
|
179
|
+
runs = target.log("mutation")
|
|
180
|
+
assert len(runs) >= 4, runs # collect, prune (batch + after the restore), strengthen-verify
|
|
181
|
+
assert all(r["mutants"] in ("absent", "empty") for r in runs), runs
|
|
182
|
+
assert all(r["KEEP_CACHE"] is None for r in runs), runs
|
|
183
|
+
assert culprit in target.ids_at(target.remote_sha(PRUNE_BRANCH))
|