Files
Open_Ultra_SAST/tests/test_push_contracts.py
T

238 lines
11 KiB
Python
Raw Normal View History

"""Approved task 1.4 contracts: persist identities without inventing completed analysis."""
import ast
import json
from dataclasses import FrozenInstanceError, replace
from pathlib import Path
import pytest
from openultrasast.config import PushConfig, config_payload, load_config
from openultrasast.contracts import Contract
from openultrasast.cpg.artifact import GraphArtifact, GraphCensus, GraphIdentity, GraphLease
from openultrasast.model.contracts import (
ChangeContext,
ChangedSpan,
DeferredQuestion,
QuestionIdentity,
RankedQuestion,
ScopeDecision,
)
from openultrasast.push.contracts import ComparisonAnalysis, PushComparison, PushResult, PushUpdate
def question(language: str = "javascript", path: str = "src/api.js") -> QuestionIdentity:
return QuestionIdentity("api", language, path, "handler", "injection")
def scope() -> ScopeDecision:
return ScopeDecision("core-v1", "evidence", True, (RankedQuestion(question(), 2.5, ("source→operation",)),), (), ())
def comparison() -> PushComparison:
return PushComparison("a" * 64, "b" * 64, "supplied_remote_tip", ("refs/heads/main",))
def test_config_round_trip_and_ordinary_scan_compatibility(tmp_path: Path) -> None:
baseline = load_config()
path = tmp_path / "settings.toml"
path.write_text('[push]\ncomparison_base = "refs/heads/main"\ndeadline_seconds = 11.5\n'
'cancellation_allowance_seconds = 1\ncache_max_bytes = 4096\nmode = "blocking"\n'
'incomplete_coverage_policy = "block"\n')
configured = load_config(path)
assert configured.push == PushConfig("refs/heads/main", 11.5, 1, 4096, "blocking", "block")
assert PushConfig.from_payload(json.loads(json.dumps(config_payload(configured)["push"]))) == configured.push
before, after = config_payload(baseline), config_payload(configured)
before.pop("push")
after.pop("push")
assert before == after
assert baseline.push == PushConfig()
assert baseline.push.deadline_seconds == 30
assert baseline.push.cancellation_allowance_seconds == 2
assert baseline.push.cache_max_bytes == 2 * 1024 ** 3
assert baseline.push.mode == "advisory"
assert baseline.push.incomplete_coverage_policy == "allow"
@pytest.mark.parametrize("setting", [
'push = "bad"', 'push = []', '[push]\ndeadline_seconds = true', '[push]\ndeadline_seconds = "30"',
'[push]\ndeadline_seconds = nan', '[push]\ndeadline_seconds = inf', '[push]\ndeadline_seconds = 0',
'[push]\ncancellation_allowance_seconds = -1', '[push]\ncache_max_bytes = 0',
'[push]\ncache_max_bytes = 1.5', '[push]\ncache_max_bytes = true', '[push]\nmode = "nag"',
'[push]\nincomplete_coverage_policy = "ignore"', '[push]\ncomparison_base = ""',
'[push]\ncomparison_base = 1', '[push]\ndeadline_second = 60',
])
def test_push_configuration_rejects_unsafe_coercion(tmp_path: Path, setting: str) -> None:
path = tmp_path / "settings.toml"
path.write_text(setting)
with pytest.raises(ValueError):
load_config(path)
def test_contract_round_trips_preserve_partition_ref_and_comparison_identity() -> None:
update = PushUpdate("refs/heads/topic", "a" * 64, "refs/heads/main", "b" * 64)
context = ChangeContext("b" * 64, "a" * 64, ("src/api.js",), ("src/guard.js",),
(ChangedSpan("src/guard.js", 3, 8, "base"),), (), ("package.json",), (), ("dynamic dispatch",))
for value in (update, comparison(), context, scope()):
assert type(value).from_payload(json.loads(json.dumps(value.to_payload()))) == value
assert question() != question("php", "src/api.js")
assert question().question_id != question("php", "src/api.js").question_id
other_base = replace(comparison(), base_oid="c" * 64)
assert other_base != comparison()
with pytest.raises(FrozenInstanceError):
update.local_oid = "other" # type: ignore[misc]
@pytest.mark.parametrize("finding,coverage,disposition", [
("none", "incomplete", "allow"), ("none", "unavailable", "block"),
("actionable", "incomplete", "allow"), ("actionable", "complete_within_scope", "block"),
])
def test_result_statuses_are_independent(finding: str, coverage: str, disposition: str) -> None:
payload = {
"analyses": [ComparisonAnalysis(comparison(), (scope(),), (question(),), coverage).to_payload()],
"finding_status": finding,
"coverage_status": coverage, "push_disposition": disposition,
"actionable_defect_ids": ["defect-1"] if finding == "actionable" else [],
}
result = PushResult.from_payload(payload)
assert PushResult.from_payload(json.loads(json.dumps(result.to_payload()))) == result
assert result.finding_status == finding
assert result.coverage_status == coverage
assert result.push_disposition == disposition
def test_incomplete_scope_cannot_be_serialized_as_complete_negative() -> None:
result = ComparisonAnalysis(comparison(), (scope(),), (question(),), "complete_within_scope")
assert ComparisonAnalysis.from_payload(result.to_payload()) == result
deferred = DeferredQuestion(question("php", "index.php"), "deadline", ("unanswered",))
for changed in (replace(scope(), deferred=(deferred,)), replace(scope(), unresolved_boundaries=("vendor call",)),
replace(scope(), population_complete=False)):
with pytest.raises(ValueError):
replace(result, scopes=(changed,))
with pytest.raises(ValueError):
replace(result, completed_questions=())
with pytest.raises(ValueError):
replace(result, scopes=())
with pytest.raises(ValueError):
replace(result, comparison=replace(comparison(), base_oid=None, base_reason="missing"))
with pytest.raises(ValueError):
replace(scope(), deferred=(DeferredQuestion(question(), "deadline", ()),))
@pytest.mark.parametrize("value", [
{"local_ref": "x", "local_oid": "a", "remote_ref": "y", "remote_oid": "b", "extra": 1},
{"local_ref": "x", "local_oid": 1, "remote_ref": "y", "remote_oid": "b"},
{"local_ref": "x", "local_oid": "a", "remote_ref": "y"},
])
def test_malformed_identity_payloads_fail(value: dict[str, object]) -> None:
with pytest.raises(ValueError):
PushUpdate.from_payload(value)
def test_graph_provenance_round_trip_and_live_lease_separation() -> None:
identity = GraphIdentity("source-digest", "declarations", "exclusions", "api", "javascript",
"4.0.625", "jssrc2cpg", "4.0.625", ("dataflowOss-v1",), (("option", "value"),))
artifact = GraphArtifact(identity, "graph-sha256", 128, GraphCensus(1, 2, 3), "complete", ())
assert GraphArtifact.from_payload(json.loads(json.dumps(artifact.to_payload()))) == artifact
assert replace(identity, exclusions_digest="changed") != identity
assert not issubclass(GraphLease, Contract) # a manifest cannot restore a live resource
with pytest.raises(ValueError):
replace(artifact, source_bytes=0)
with pytest.raises(ValueError):
replace(artifact, diagnostics=("file unreadable",))
with pytest.raises(ValueError):
replace(artifact, census=GraphCensus(0, 0, 0))
with pytest.raises(ValueError):
replace(identity, overlay_options=(("x", "1"), ("x", "2")))
def test_strict_runtime_construction_not_only_decoding() -> None:
with pytest.raises(ValueError):
RankedQuestion(question(), float("nan"), ())
with pytest.raises(ValueError):
ChangedSpan("x", True, 3, "head")
with pytest.raises(ValueError):
ChangedSpan("x", 4, 3, "head")
with pytest.raises(ValueError):
PushConfig(deadline_seconds=True)
with pytest.raises(ValueError):
PushConfig.from_payload({"mode": "blocking", "incomplete_coverage_policy": []})
def test_generic_contracts_and_engine_do_not_depend_on_push() -> None:
root = Path("src/openultrasast")
paths = [root / "contracts.py", *(root / "model").glob("*.py"), *(root / "cpg").glob("*.py")]
for path in paths:
for node in ast.walk(ast.parse(path.read_text())):
if isinstance(node, ast.ImportFrom):
assert "push" not in (node.module or "").split("."), path
elif isinstance(node, ast.Import):
assert all("push" not in alias.name.split(".") for alias in node.names), path
def test_shared_head_does_not_merge_different_base_completion() -> None:
completed = ComparisonAnalysis(comparison(), (scope(),), (question(),), "complete_within_scope")
pending = ComparisonAnalysis(replace(comparison(), base_oid="c" * 64), (scope(),), (), "incomplete")
result = PushResult((completed, pending), "none", "incomplete", "allow", ())
restored = PushResult.from_payload(json.loads(json.dumps(result.to_payload())))
assert restored == result
assert restored.analyses[1].completed_questions == ()
with pytest.raises(ValueError):
replace(result, coverage_status="complete_within_scope")
with pytest.raises(ValueError):
replace(pending, coverage_status="complete_within_scope")
def test_deletion_only_result_is_explicitly_not_applicable() -> None:
result = PushResult((), "none", "not_applicable", "allow", ())
assert PushResult.from_payload(result.to_payload()) == result
with pytest.raises(ValueError):
replace(result, coverage_status="complete_within_scope")
@pytest.mark.parametrize("field,value", [
("finding_status", "clean"), ("coverage_status", "secure"), ("push_disposition", "warn"),
("finding_status", True), ("coverage_status", None),
])
def test_result_rejects_unknown_status_vocabulary(field: str, value: object) -> None:
payload = PushResult((), "none", "not_applicable", "allow", ()).to_payload()
payload[field] = value
with pytest.raises(ValueError):
PushResult.from_payload(payload)
def test_graph_partial_provenance_stays_partial_and_options_are_preserved() -> None:
identity = GraphIdentity("sources", "decls", "excludes", "core", "c", "4.0.625", "c2cpg", "4.0.625",
("dataflowOss-v1",), (("depth", "2"),), (("include", "src"),))
artifact = GraphArtifact(identity, "digest", 0, GraphCensus(0, 0, 0), "partial", ("source unreadable",))
restored = GraphArtifact.from_payload(json.loads(json.dumps(artifact.to_payload())))
assert restored == artifact
assert restored.identity.frontend_options == (("include", "src"),)
with pytest.raises(ValueError):
replace(restored, completeness="complete")
def test_execution_budget_requires_finite_clock_and_distinct_positive_allowance() -> None:
from openultrasast.model.contracts import ExecutionBudget
budget = ExecutionBudget(123.0, 2.0)
assert budget.deadline_monotonic == 123
assert budget.cancellation_allowance_seconds == 2
for deadline, allowance in ((float("inf"), 2), (1, float("nan")), (True, 2), (1, 0), (-1, 2)):
with pytest.raises(ValueError):
ExecutionBudget(deadline, allowance)
def test_empty_population_needs_an_explicit_census_assertion() -> None:
empty = ScopeDecision("core-v1", "evidence", True, (), (), ())
analyzed = ComparisonAnalysis(comparison(), (empty,), (), "complete_within_scope")
assert analyzed.completed_questions == ()
with pytest.raises(ValueError):
replace(analyzed, scopes=(replace(empty, population_complete=False),))
def test_direct_mutable_collection_is_not_an_immutable_contract() -> None:
with pytest.raises(ValueError):
PushComparison("head", "base", "supplied", ["refs/heads/main"]) # type: ignore[arg-type]
with pytest.raises(ValueError):
PushResult.from_payload({"finding_status": "none"})