mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(prover): access-control prover, delivery A: path records, Stapler and Jetty entries, Java operation rows, the prover and its scoring arm
Codex delivery under spec access-control-prover. `semantic/obligations/paths.py` builds entry-to-function path records from the cached caller index; `mapping.py` names Stapler web methods and Jetty handlers as entries; `check_obligations` gains the opt-in premise `assume_entries_protected` with the label `unguarded_path`; `hunt/prover.py` proves a change with no model call and states its coverage; `benchmarks/valid/prover_arm.py` scores it with the valid-benchmark metrics. Two fixes after the first smoke run: a product file git holds empty is read, not unread (four changes failed on `__init__.py` and empty assets), and absent-input reasons are counted by name. Smoke on the 23 fix changes of the training pool: 7 pushes caught (30 percent, Wilson 16 to 51), 28 of 93 locations, precision 7 of 8 flagged, no instrument failure; three changes over the 120 second bound. Twelve of the 17 Jenkins fixes reach the fixed method but check no operation because the method is the operation; the spec now carries handler-named operations for delivery B. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e514551cb6
commit
beb272a5ec
@@ -0,0 +1,183 @@
|
||||
"""Prover integration on instrumented local Git fixtures."""
|
||||
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.hunt.producers import Context
|
||||
from openultrasast.hunt.prover import prove_change
|
||||
|
||||
|
||||
def repository(tmp_path, text, extra=None, lines=None):
|
||||
def git(*args):
|
||||
return subprocess.run(
|
||||
["git", "-C", str(tmp_path), "-c", "core.hooksPath=/dev/null", *args], check=True, capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
|
||||
git("init", "-q")
|
||||
git("config", "user.name", "Fixture")
|
||||
git("config", "user.email", "[email protected]")
|
||||
(tmp_path / "Fixture.java").write_text("class Plain {}\n")
|
||||
git("add", "Fixture.java")
|
||||
git("commit", "-qm", "base")
|
||||
base = git("rev-parse", "HEAD")
|
||||
(tmp_path / "Fixture.java").write_text(text)
|
||||
for path, value in (extra or {}).items():
|
||||
(tmp_path / path).parent.mkdir(parents=True, exist_ok=True)
|
||||
(tmp_path / path).write_text(value)
|
||||
git("add", path)
|
||||
git("add", "Fixture.java")
|
||||
git("commit", "-qm", "head")
|
||||
change = dict(
|
||||
id="fixture",
|
||||
repo="fixture/app",
|
||||
kind="ordinary",
|
||||
base=base,
|
||||
head=git("rev-parse", "HEAD"),
|
||||
files=[{"path": "Fixture.java", "head_lines": lines or [[1, len(text.splitlines())]]}],
|
||||
)
|
||||
return tmp_path, change
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard,findings", [("", 1), ("Jenkins.get().checkPermission(Jenkins.ADMINISTER);", 0)])
|
||||
def test_stapler_verdict(tmp_path, guard, findings):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
f"""class X extends Descriptor<Thing> {{
|
||||
public Object doTestConnection() {{
|
||||
{guard}
|
||||
return CredentialsProvider.lookupCredentials(Thing.class);
|
||||
}}
|
||||
}}""",
|
||||
)
|
||||
ctx = Context(root, change)
|
||||
result = prove_change(root, change, ctx)
|
||||
assert len(result.findings) == findings
|
||||
assert result.coverage.operations_checked == 1
|
||||
assert result.coverage.operations_discharged == 1 - findings
|
||||
assert result.coverage.reached == 1
|
||||
assert len(ctx.read("Fixture.java").encode()) > 0
|
||||
if findings:
|
||||
(finding,) = result.findings
|
||||
assert finding["family"] == "access_control" and finding["function"] == "doTestConnection"
|
||||
assert finding["id"] == "obligation:protected_read:Fixture.java:4:path_guard"
|
||||
assert finding["entry"]["kind"] == "route" and finding["hops"] == [["Fixture.java::doTestConnection", None]]
|
||||
assert finding["operation"]["calls"] == ["lookupCredentials"] and finding["missing"] == "path_guard"
|
||||
assert finding["evidence"]
|
||||
|
||||
|
||||
def test_unrecognised_is_coverage_only(tmp_path):
|
||||
root, change = repository(tmp_path, "class Plain { public Object run() { return lookupCredentials(); } }")
|
||||
result = prove_change(root, change)
|
||||
assert not result.findings and result.coverage.frameworks_recognised == {}
|
||||
assert result.coverage.unchecked_by_reason["framework_unrecognised"] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard_position,expected", [("before", 0), ("after", 1), ("none", 1)])
|
||||
def test_callee_and_caller_guard_order(tmp_path, guard_position, expected):
|
||||
guard = "Jenkins.get().checkPermission(Jenkins.ADMINISTER);"
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
f"""class X extends Descriptor<Thing> {{
|
||||
public Object doTestConnection() {{
|
||||
{guard if guard_position == "before" else ""}
|
||||
Object result = Helper.readSecret();
|
||||
{guard if guard_position == "after" else ""}
|
||||
return result;
|
||||
}}
|
||||
}}""",
|
||||
{"Helper.java": "class Helper { public static Object readSecret() { return lookupCredentials(); } }"},
|
||||
[[2, 7]],
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == expected and result.coverage.operations_checked == 1
|
||||
if expected:
|
||||
assert result.findings[0]["path"] == "Helper.java"
|
||||
assert len(result.findings[0]["hops"]) == 2
|
||||
|
||||
|
||||
def test_touched_helper_walks_to_entry(tmp_path):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"""class X extends Descriptor<Thing> {
|
||||
public Object doTestConnection() { return readSecret(); }
|
||||
public Object readSecret() { return lookupCredentials(); }
|
||||
}""",
|
||||
lines=[[3, 3]],
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == 1 and len(result.findings[0]["hops"]) == 2
|
||||
|
||||
|
||||
def test_all_paths_are_checked(tmp_path):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"""class X extends Descriptor<Thing> {
|
||||
public Object doFirst() {
|
||||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||||
return readSecret();
|
||||
}
|
||||
public Object doSecond() { return readSecret(); }
|
||||
public Object readSecret() { return lookupCredentials(); }
|
||||
}""",
|
||||
lines=[[7, 7]],
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == 1 and result.coverage.operations_discharged == 0
|
||||
|
||||
|
||||
def test_generated_output_and_parse_failure_counted(tmp_path):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"class X { public Object broken( { return lookupCredentials(); }",
|
||||
{"dist/bundle.java": "class X { public void doTest() {} }"},
|
||||
)
|
||||
change["files"].append({"path": "dist/bundle.java", "head_lines": [[1, 1]]})
|
||||
result = prove_change(root, change)
|
||||
assert not result.findings and result.coverage.unchecked_by_reason["parse_failed"] >= 1
|
||||
assert result.coverage.unchecked_by_reason["excluded_dir"] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exits,expected", [(True, 0), (False, 1)])
|
||||
def test_caller_permission_predicate_requires_exit(tmp_path, exits, expected):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
f"""class X extends Descriptor<Thing> {{
|
||||
public Object doTestConnection() {{
|
||||
boolean allowed = Jenkins.get().hasPermission(Jenkins.ADMINISTER);
|
||||
{"if (!allowed) {" if exits else ""}
|
||||
{"return null;" if exits else ""}
|
||||
{"}" if exits else ""}
|
||||
return Helper.readSecret();
|
||||
}}
|
||||
}}""",
|
||||
{"Helper.java": "class Helper { public static Object readSecret() { return lookupCredentials(); } }"},
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == expected and result.coverage.reached == 1
|
||||
assert result.coverage.operations_discharged == 1 - expected
|
||||
|
||||
|
||||
def test_declared_public_route_suppresses_premise(tmp_path):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"""class X extends Descriptor<Thing> {
|
||||
public Object doTestConnection() { return lookupCredentials(); }
|
||||
}""",
|
||||
{".openultrasast/obligations.toml": 'version = 1\n[[route]]\npath = "doTestConnection"\naccess = "public"\n'},
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert not result.findings and result.coverage.operations_checked == 1
|
||||
assert result.coverage.operations_discharged == 0
|
||||
|
||||
|
||||
def test_nonpublic_unreached_function_in_partly_covered_repo(tmp_path):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"class Plain { public Object run() { return lookupCredentials(); } }",
|
||||
{"Exposed.java": "class X extends Descriptor<Thing> { public void doTest() {} }"},
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert result.coverage.frameworks_recognised == {"stapler": 1}
|
||||
assert result.coverage.unchecked_by_reason["framework_unrecognised"] == 1
|
||||
assert not result.findings
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Java framework entries are tied to their declaring class and public method."""
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.mapping import analyze_entry_points
|
||||
from openultrasast.preprocess import build_file_target
|
||||
|
||||
|
||||
def entries(tmp_path, text):
|
||||
path = tmp_path / "Fixture.java"
|
||||
path.write_text(text)
|
||||
return [e for e in analyze_entry_points(tmp_path, [build_file_target(tmp_path, path)]) if e.function_name]
|
||||
|
||||
|
||||
def test_stapler_class_scope_visibility_and_ranges(tmp_path):
|
||||
rows = entries(
|
||||
tmp_path,
|
||||
"""class Exposed extends Descriptor<Thing> {
|
||||
public String doTestConnection() {
|
||||
return lookupCredentials();
|
||||
}
|
||||
private void doHidden() {}
|
||||
public void ordinary() {}
|
||||
}
|
||||
class Plain {
|
||||
public void doUnexposed() {}
|
||||
public void doCheck(@QueryParameter String value) {}
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert [(e.function_name, e.line, e.end_line) for e in rows] == [("doTestConnection", 2, 4), ("doCheck", 10, 10)]
|
||||
assert all(e.access_level == "unknown" and e.trust_boundary == "http_request" and e.access_evidence for e in rows)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("base", ["Action", "RootAction", "Plugin", "AbstractDescribableImpl<Thing>", "Descriptor<Thing>"])
|
||||
def test_exposed_types(tmp_path, base):
|
||||
assert len(entries(tmp_path, f"class X extends {base} {{ public void doCheckX() {{}} }}")) == 1
|
||||
|
||||
|
||||
def test_unprotected_root_and_stapler_parameters(tmp_path):
|
||||
rows = entries(
|
||||
tmp_path,
|
||||
"""class Open implements UnprotectedRootAction {
|
||||
public void doTest() {}
|
||||
}
|
||||
class Plain {
|
||||
public void doCheck(StaplerRequest2 request, StaplerResponse response) {}
|
||||
public void doLowercase() {}
|
||||
}""",
|
||||
)
|
||||
assert [e.access_level for e in rows] == ["public", "unknown"]
|
||||
assert rows[0].access_declared
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text,name",
|
||||
[
|
||||
("class X extends AbstractHandler { public void handle(String target) {} }", "handle"),
|
||||
("class X extends HttpServlet { protected void doGet(Request r) {} }", "doGet"),
|
||||
('@Path("/api") class X { @GET public String read() { return "ok"; } }', "read"),
|
||||
],
|
||||
)
|
||||
def test_jetty_servlet_and_jaxrs(tmp_path, text, name):
|
||||
(row,) = entries(tmp_path, text)
|
||||
assert row.function_name == name and row.kind == "route" and row.access_evidence
|
||||
|
||||
|
||||
def test_unrecognised_and_comments(tmp_path):
|
||||
assert entries(tmp_path, "/* class X extends Descriptor { public void doFake() {} } */\nclass Plain { public void doCheck() {} }") == []
|
||||
@@ -206,3 +206,34 @@ def test_a_malformed_store_row_is_a_degradation_not_a_crash() -> None:
|
||||
(degradation,) = [d for d in result.degradations if d["reason"] == "obligations_store_row_invalid"]
|
||||
assert degradation["records"] == ["corpus:bad", "corpus:worse"]
|
||||
assert all(f.known_fix is None for f in result.findings)
|
||||
|
||||
|
||||
def test_recognised_entry_premise_is_opt_in_and_never_applies_to_public():
|
||||
from dataclasses import replace
|
||||
|
||||
from openultrasast.semantic.obligations import load_obligation_facts
|
||||
from openultrasast.semantic.obligations.check import check_obligations, findings_to_static
|
||||
from openultrasast.semantic.obligations.dominance import OrderDominance
|
||||
|
||||
text = "def exposed(title):\n return Book.query.filter_by(book_title=title).first()\n"
|
||||
entry = replace(_entry("exposed", 1, 2, "authenticated", ["framework entry"]), access_level="unknown")
|
||||
kwargs = dict(
|
||||
irs={"app.py": (parse_file("app.py", text, "python"), text)},
|
||||
entries=[entry],
|
||||
facts=load_obligation_facts(),
|
||||
flow_facts=load_facts(),
|
||||
policy=None,
|
||||
paths=[_Path((("app.py::exposed", 1),))],
|
||||
dominance=OrderDominance({"app.py": text}),
|
||||
store_shapes=(),
|
||||
min_siblings=3,
|
||||
)
|
||||
assert check_obligations(**kwargs).findings == ()
|
||||
result = check_obligations(**kwargs, assume_entries_protected=True)
|
||||
(finding,) = result.findings
|
||||
assert finding.label == "unguarded_path"
|
||||
assert "no declared policy clause and no sibling norm" in "; ".join(finding.evidence)
|
||||
assert "framework entry" in "; ".join(finding.evidence)
|
||||
assert "unguarded_path" in findings_to_static(result)[0].rationale
|
||||
kwargs["entries"] = [replace(entry, access_level="public")]
|
||||
assert check_obligations(**kwargs, assume_entries_protected=True).findings == ()
|
||||
|
||||
@@ -91,3 +91,27 @@ def test_java_php_facts_resolve_operations_and_dischargers(language, text, opera
|
||||
assert ir.parse_ok
|
||||
assert operation in {op.fact_id for op in find_operations(ir, facts, text=text)}
|
||||
assert discharger in {d.fact_id for d in find_discharges(ir, facts, load_facts().for_language(language), text=text)}
|
||||
|
||||
|
||||
def test_delivery_a_java_operations_and_dischargers_resolve():
|
||||
from openultrasast.semantic.obligations.facts import load_obligation_facts
|
||||
|
||||
facts = load_obligation_facts().for_language("java")
|
||||
operations = {call: fact for fact in facts.operations for call in fact.calls}
|
||||
dischargers = {call: fact for fact in facts.dischargers for call in fact.calls}
|
||||
for call in (
|
||||
"lookupCredentials",
|
||||
"lookupCredentialsInItem",
|
||||
"getCredentialsFromId",
|
||||
"testConnection",
|
||||
"verifyCredentials",
|
||||
"listPropertiesOfSecrets",
|
||||
"openConnection",
|
||||
"connect",
|
||||
"publish",
|
||||
"getItemByFullName",
|
||||
"updateByXml",
|
||||
):
|
||||
assert "path_guard" in operations[call].requires
|
||||
assert dischargers["checkPermission"].kind == dischargers["checkAnyPermission"].kind == "path_guard"
|
||||
assert dischargers["hasPermission"].kind == "ownership_check"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Bounded paths preserve file identity and call-site lines."""
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.semantic.obligations.paths import PathRecord, compose_paths
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth,reached", [(0, False), (1, False), (2, True), (3, True)])
|
||||
def test_depth_and_call_sites(depth, reached):
|
||||
graph = {"c.java::target": [("b.java::helper", 8)], "b.java::helper": [("a.java::doTest", 4)]}
|
||||
paths, unreached = compose_paths(
|
||||
callers=lambda p, f: graph.get(f"{p}::{f}", []), entries={"a.java::doTest"}, targets=[("c.java", "target")], max_depth=depth
|
||||
)
|
||||
assert bool(paths) == reached
|
||||
assert unreached == ([] if reached else ["c.java::target"])
|
||||
if reached:
|
||||
assert paths == [PathRecord((("a.java::doTest", 4), ("b.java::helper", 8), ("c.java::target", None)), "a.java::doTest", True)]
|
||||
|
||||
|
||||
def test_entry_dedupe_cycles_fan_out_and_file_identity():
|
||||
graph = {"c.java::save": [("b.java::helper", 8), ("a.java::doTest", 4), ("a.java::doTest", 4)], "b.java::helper": [("c.java::save", 5)]}
|
||||
kwargs = dict(
|
||||
callers=lambda p, f: graph.get(f"{p}::{f}", []),
|
||||
entries={"a.java::doTest"},
|
||||
targets=[("c.java", "save"), ("other.java", "save"), ("a.java", "doTest"), ("c.java", "save")],
|
||||
)
|
||||
paths, unreached = compose_paths(**kwargs)
|
||||
assert len(paths) == 2 and unreached == ["other.java::save"]
|
||||
assert any(p.hops == (("a.java::doTest", None),) for p in paths)
|
||||
assert compose_paths(**kwargs, fan_out=1)[1] == ["c.java::save", "other.java::save"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key,value", [("max_depth", -1), ("fan_out", 0)])
|
||||
def test_invalid_bounds(key, value):
|
||||
with pytest.raises(ValueError):
|
||||
compose_paths(callers=lambda p, f: [], entries=set(), targets=[], **{key: value})
|
||||
|
||||
|
||||
def test_records_are_consumed_by_the_checker():
|
||||
from types import SimpleNamespace
|
||||
|
||||
from openultrasast.semantic.facts import load_facts
|
||||
from openultrasast.semantic.ir import parse_file
|
||||
from openultrasast.semantic.obligations.check import check_obligations
|
||||
from openultrasast.semantic.obligations.dominance import OrderDominance
|
||||
from openultrasast.semantic.obligations.facts import load_obligation_facts
|
||||
|
||||
text = "def entry(title):\n return Book.query.filter_by(book_title=title).first()\n"
|
||||
paths, _ = compose_paths(callers=lambda p, f: [], entries={"app.py::entry"}, targets=[("app.py", "entry")])
|
||||
result = check_obligations(
|
||||
irs={"app.py": (parse_file("app.py", text, "python"), text)},
|
||||
entries=[
|
||||
SimpleNamespace(path="app.py", function_name="entry", access_level="unknown", kind="route", access_evidence=["fixture entry"])
|
||||
],
|
||||
facts=load_obligation_facts(),
|
||||
flow_facts=load_facts(),
|
||||
policy=None,
|
||||
paths=paths,
|
||||
dominance=OrderDominance({"app.py": text}),
|
||||
store_shapes=(),
|
||||
min_siblings=3,
|
||||
assume_entries_protected=True,
|
||||
)
|
||||
(finding,) = result.findings
|
||||
assert finding.operation.function == "entry" and finding.label == "unguarded_path"
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Offline replay, resume, scoring and instrument refusal for the prover arm."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from test_hunt_prover import repository # noqa: E402
|
||||
|
||||
from benchmarks.unseen import score # noqa: E402
|
||||
from benchmarks.valid import prover_arm # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pool(tmp_path, monkeypatch):
|
||||
def forbidden(*args, **kwargs):
|
||||
pytest.fail("network access attempted")
|
||||
|
||||
monkeypatch.setattr(socket.socket, "connect", forbidden)
|
||||
root = tmp_path / "repo"
|
||||
root.mkdir()
|
||||
text = """class X extends Descriptor<Thing> {
|
||||
public Object doTestConnection() {
|
||||
return lookupCredentials();
|
||||
}
|
||||
}"""
|
||||
root, positive = repository(root, text)
|
||||
positive.update(
|
||||
id="positive",
|
||||
kind="vulnerability",
|
||||
family="access_control",
|
||||
path="Fixture.java",
|
||||
function="doTestConnection",
|
||||
function_lines=[2, 4],
|
||||
targets=[{"path": "Fixture.java", "function": "doTestConnection", "function_lines": [2, 4], "family": "access_control"}],
|
||||
)
|
||||
(root / "Fixture.java").write_text(text.replace(" return", " Jenkins.get().checkPermission(Jenkins.ADMINISTER);\n return"))
|
||||
subprocess.run(["git", "-C", str(root), "add", "Fixture.java"], check=True)
|
||||
subprocess.run(["git", "-C", str(root), "-c", "core.hooksPath=/dev/null", "commit", "-qm", "safe"], check=True)
|
||||
head = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], check=True, capture_output=True, text=True).stdout.strip()
|
||||
ordinary = {
|
||||
**positive,
|
||||
"id": "ordinary",
|
||||
"kind": "ordinary",
|
||||
"base": positive["head"],
|
||||
"head": head,
|
||||
"targets": [],
|
||||
"files": [{"path": "Fixture.java", "head_lines": [[3, 3]]}],
|
||||
}
|
||||
changes = [positive, ordinary]
|
||||
manifest = tmp_path / "frozen.json"
|
||||
manifest.write_text(
|
||||
json.dumps({"changes": changes, "sha256": hashlib.sha256(json.dumps(changes, sort_keys=True).encode()).hexdigest()})
|
||||
)
|
||||
repos = tmp_path / "repos.json"
|
||||
repos.write_text(json.dumps({"fixture/app": str(root)}))
|
||||
return root, manifest, repos, tmp_path / "out"
|
||||
|
||||
|
||||
def test_run_resume_summary_and_locations(pool, monkeypatch, capsys):
|
||||
root, manifest, repos, out = pool
|
||||
assert prover_arm.main(["run", str(manifest), "--repos", str(repos), "--out", str(out)]) == 0
|
||||
rows = score.load_records(out)
|
||||
assert len(rows) == 2 and all(r["done"] and r["seconds"] > 0 for r in rows)
|
||||
assert score.catches(rows[0]) == ["alerts"] and not score.false_alarm(rows[1])
|
||||
summary = json.loads((out / "summary.json").read_text())
|
||||
assert summary["recall"]["count"] == summary["recall"]["n"] == 1
|
||||
assert summary["location_recall"]["count"] == summary["location_recall"]["n"] == 1
|
||||
assert summary["false_alarms_per_100_pushes"]["rate"] == 0
|
||||
assert summary["precision"]["rate"] == 1
|
||||
assert summary["instrument_failures"] == 0
|
||||
assert "fixture/app" in summary["per_repository"] and "stapler" in summary["per_framework"]
|
||||
assert summary["changes_over_120_seconds"] == [] and summary["uncovered_repositories"] == []
|
||||
assert summary["seconds_per_change"]["n"] == 2
|
||||
assert "bytes read" in capsys.readouterr().out
|
||||
old = {p.name: p.read_bytes() for p in out.glob("*.json") if p.name not in {"inputs.json", "summary.json"}}
|
||||
|
||||
def forbidden(*args, **kwargs):
|
||||
pytest.fail("completed change re-run")
|
||||
|
||||
monkeypatch.setattr(prover_arm, "prove_change", forbidden)
|
||||
assert prover_arm.run(manifest, repos, out) == 0
|
||||
assert old == {p.name: p.read_bytes() for p in out.glob("*.json") if p.name not in {"inputs.json", "summary.json"}}
|
||||
assert (
|
||||
len(
|
||||
subprocess.run(
|
||||
["git", "-C", str(root), "worktree", "list", "--porcelain"], check=True, capture_output=True, text=True
|
||||
).stdout.split("worktree ")
|
||||
)
|
||||
== 2
|
||||
)
|
||||
|
||||
|
||||
def test_zero_byte_record_refuses(pool):
|
||||
_, manifest, repos, out = pool
|
||||
assert prover_arm.run(manifest, repos, out) == 0
|
||||
path = out / (hashlib.sha256(b"positive").hexdigest() + ".json")
|
||||
record = json.loads(path.read_text())
|
||||
record["instrument"]["changed_bytes"]["Fixture.java"] = 0
|
||||
path.write_text(json.dumps(record))
|
||||
with pytest.raises(ValueError, match="zero-byte"):
|
||||
prover_arm.summarize(out)
|
||||
|
||||
|
||||
def test_checkout_error_is_type_only(pool):
|
||||
_, manifest, repos, out = pool
|
||||
repos.write_text("{}")
|
||||
assert prover_arm.run(manifest, repos, out) == 2
|
||||
rows = score.load_records(out)
|
||||
assert all(row["status"] == "instrument_failure" and row["instrument"]["failure"] == "KeyError" for row in rows)
|
||||
|
||||
|
||||
def test_resume_rejects_new_depth(pool):
|
||||
_, manifest, repos, out = pool
|
||||
assert prover_arm.run(manifest, repos, out) == 0
|
||||
with pytest.raises(ValueError, match="inputs"):
|
||||
prover_arm.run(manifest, repos, out, max_depth=2)
|
||||
|
||||
|
||||
def test_unchanged_file_empty_read_refuses(pool, monkeypatch):
|
||||
_, manifest, repos, out = pool
|
||||
real = prover_arm.Context.read
|
||||
|
||||
def empty(ctx, path):
|
||||
if path == "Fixture.java":
|
||||
return ""
|
||||
return real(ctx, path)
|
||||
|
||||
monkeypatch.setattr(prover_arm.Context, "read", empty)
|
||||
assert prover_arm.run(manifest, repos, out) == 2
|
||||
with pytest.raises(ValueError, match="zero-byte"):
|
||||
prover_arm.summarize(out)
|
||||
Reference in New Issue
Block a user