mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 17:58:18 +00:00
feat(access-control-prover): 9.3 summary caches and explicit exploration cutoffs
This commit is contained in:
@@ -150,7 +150,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
|
|||||||
- _Requirements: 3.7, 11.6_
|
- _Requirements: 3.7, 11.6_
|
||||||
- _Boundary: Function Summaries_
|
- _Boundary: Function Summaries_
|
||||||
- _Depends: 9.1_
|
- _Depends: 9.1_
|
||||||
- [ ] 9.3 Add summary caches and explicit exploration cutoffs
|
- [x] 9.3 Add summary caches and explicit exploration cutoffs
|
||||||
- Reuse summaries per change and carry depth, fan-out, deadline and other cutoff reasons into completeness.
|
- Reuse summaries per change and carry depth, fan-out, deadline and other cutoff reasons into completeness.
|
||||||
- Done when each cutoff fixture returns accumulated evidence plus the exact incomplete reason.
|
- Done when each cutoff fixture returns accumulated evidence plus the exact incomplete reason.
|
||||||
- _Requirements: 3.8, 6.2, 6.3, 7.2_
|
- _Requirements: 3.8, 6.2, 6.3, 7.2_
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ from .. import mapping, source
|
|||||||
from ..model.taxonomy import DEFAULT_FAMILIES_PATH, load_families
|
from ..model.taxonomy import DEFAULT_FAMILIES_PATH, load_families
|
||||||
from ..preprocess import build_file_target, detect_language
|
from ..preprocess import build_file_target, detect_language
|
||||||
from ..semantic.facts import load_facts
|
from ..semantic.facts import load_facts
|
||||||
from ..semantic.ir import FileIR, parse_file
|
from ..semantic.ir import FileIR, FunctionIR, parse_file
|
||||||
from ..semantic.obligations import check_obligations, load_obligation_facts, resolution
|
from ..semantic.obligations import check_obligations, load_obligation_facts, resolution
|
||||||
|
from ..semantic.obligations import summaries as summaries_module
|
||||||
from ..semantic.obligations.dominance import OrderDominance
|
from ..semantic.obligations.dominance import OrderDominance
|
||||||
from ..semantic.obligations.operations import covers, find_operations, valid_witness
|
from ..semantic.obligations.operations import covers, find_operations, valid_witness
|
||||||
from .labels import label_rows
|
from .labels import label_rows
|
||||||
@@ -116,6 +117,20 @@ class Context:
|
|||||||
files.append((path, text, language))
|
files.append((path, text, language))
|
||||||
return resolution.SymbolIndex.build(files)
|
return resolution.SymbolIndex.build(files)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def summaries(self) -> tuple[summaries_module.FunctionSummary, ...]:
|
||||||
|
"""Monotone function summaries over the product tree, computed once per change (Req 6.2)."""
|
||||||
|
functions_by_module: dict[str, list[FunctionIR]] = {}
|
||||||
|
for path in self.product_files:
|
||||||
|
try:
|
||||||
|
file_ir = self.ir(path)
|
||||||
|
except _Absent:
|
||||||
|
# Same guard as entry_points and symbol_index: an unreadable product file
|
||||||
|
# contributes no functions, not a failure of the summary cache.
|
||||||
|
continue
|
||||||
|
functions_by_module.setdefault(path, []).extend(file_ir.functions)
|
||||||
|
return summaries_module.monotone_summaries(functions_by_module, index=self.symbol_index)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def hunks(self) -> dict[str, list[list[int]]]:
|
def hunks(self) -> dict[str, list[list[int]]]:
|
||||||
"""Line ranges per file that the push changed or added, at head."""
|
"""Line ranges per file that the push changed or added, at head."""
|
||||||
|
|||||||
@@ -5,18 +5,26 @@ operations, local witnesses, resolved calls and local completeness WITHOUT
|
|||||||
traversing callers. Task 9.2 (Reqs 3.7, 11.6): a monotone fixpoint over
|
traversing callers. Task 9.2 (Reqs 3.7, 11.6): a monotone fixpoint over
|
||||||
strongly connected call components unions callee evidence into callers until
|
strongly connected call components unions callee evidence into callers until
|
||||||
stable, so direct and mutual recursion terminate with the accumulated
|
stable, so direct and mutual recursion terminate with the accumulated
|
||||||
evidence. Summaries are deterministic and carry no target names beyond fact
|
evidence. Task 9.3 (Reqs 3.8, 6.2, 6.3, 7.2): an exploration budget bounds
|
||||||
identifiers (Req 11.7).
|
the fixpoint; depth, fan-out and deadline cutoffs stop growth, record the
|
||||||
|
exact cutoff reason and leave the affected summaries incomplete while
|
||||||
|
preserving the evidence accumulated so far. Summaries are deterministic and
|
||||||
|
carry no target names beyond fact identifiers (Req 11.7).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Iterable, Iterator, Mapping
|
from collections.abc import Iterable, Iterator, Mapping
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field, replace
|
||||||
|
from time import monotonic
|
||||||
|
|
||||||
from ..ir import FunctionIR
|
from ..ir import FunctionIR
|
||||||
from .abstract_domain import Completeness, join_completeness
|
from .abstract_domain import Completeness, join_completeness
|
||||||
from .operations import Discharge, Operation
|
from .operations import Discharge, Operation
|
||||||
from .resolution import CallTargetSet, SymbolId, SymbolIndex, resolve_call
|
from .resolution import CallTargetSet, SymbolId, SymbolIndex, resolve_call
|
||||||
|
|
||||||
|
_DEPTH_CUTOFF = "summaries:depth_cutoff"
|
||||||
|
_FAN_OUT_CUTOFF = "summaries:fan_out_cutoff"
|
||||||
|
_DEADLINE_CUTOFF = "summaries:deadline_cutoff"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class FunctionSummary:
|
class FunctionSummary:
|
||||||
@@ -29,6 +37,20 @@ class FunctionSummary:
|
|||||||
completeness: Completeness = field(default_factory=lambda: Completeness(is_complete=True))
|
completeness: Completeness = field(default_factory=lambda: Completeness(is_complete=True))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ExplorationBudget:
|
||||||
|
"""Work bounds for the summary fixpoint; ``None`` means unbounded.
|
||||||
|
|
||||||
|
Req 3.8: every bound that truncates exploration records its exact cutoff
|
||||||
|
reason and leaves affected summaries incomplete with the evidence
|
||||||
|
accumulated so far.
|
||||||
|
"""
|
||||||
|
|
||||||
|
depth: int | None = None
|
||||||
|
fan_out: int | None = None
|
||||||
|
deadline: float | None = None
|
||||||
|
|
||||||
|
|
||||||
def _enclosing_type(index: SymbolIndex, module: str, name: str) -> str | None:
|
def _enclosing_type(index: SymbolIndex, module: str, name: str) -> str | None:
|
||||||
"""Return the enclosing type of the function ``name`` declared in ``module``."""
|
"""Return the enclosing type of the function ``name`` declared in ``module``."""
|
||||||
for symbol in index.by_name.get(name, ()):
|
for symbol in index.by_name.get(name, ()):
|
||||||
@@ -245,22 +267,80 @@ def _propagated(summary: FunctionSummary, callees: Iterable[FunctionSummary]) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _callees_for(
|
||||||
|
member_symbol: SymbolId,
|
||||||
|
edges: Mapping[SymbolId, tuple[SymbolId, ...]],
|
||||||
|
summaries_by_symbol: Mapping[SymbolId, FunctionSummary],
|
||||||
|
fan_out: int | None,
|
||||||
|
) -> tuple[list[FunctionSummary], bool]:
|
||||||
|
"""Resolve a member's exact callees, truncated to ``fan_out`` when set."""
|
||||||
|
callee_edges = [edge for edge in edges.get(member_symbol, ()) if edge in summaries_by_symbol]
|
||||||
|
truncated = fan_out is not None and len(callee_edges) > fan_out
|
||||||
|
if truncated:
|
||||||
|
callee_edges = callee_edges[:fan_out]
|
||||||
|
return [summaries_by_symbol[edge] for edge in callee_edges], truncated
|
||||||
|
|
||||||
|
|
||||||
|
def _propagate_round(
|
||||||
|
members: tuple[FunctionSummary, ...],
|
||||||
|
summaries_by_symbol: dict[SymbolId, FunctionSummary],
|
||||||
|
edges: Mapping[SymbolId, tuple[SymbolId, ...]],
|
||||||
|
budget: ExplorationBudget,
|
||||||
|
) -> bool:
|
||||||
|
"""One union round over the component; returns whether anything changed."""
|
||||||
|
changed = False
|
||||||
|
for member in members:
|
||||||
|
current = summaries_by_symbol[member.symbol]
|
||||||
|
callees, truncated = _callees_for(member.symbol, edges, summaries_by_symbol, budget.fan_out)
|
||||||
|
propagated = _propagated(current, callees)
|
||||||
|
if truncated:
|
||||||
|
propagated = replace(
|
||||||
|
propagated,
|
||||||
|
completeness=join_completeness(
|
||||||
|
propagated.completeness,
|
||||||
|
Completeness(is_complete=False, reasons=(_FAN_OUT_CUTOFF,)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if propagated != current:
|
||||||
|
summaries_by_symbol[member.symbol] = propagated
|
||||||
|
changed = True
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_cutoff_reason(
|
||||||
|
members: tuple[FunctionSummary, ...],
|
||||||
|
summaries_by_symbol: dict[SymbolId, FunctionSummary],
|
||||||
|
reason: str,
|
||||||
|
) -> None:
|
||||||
|
"""Mark every member of a truncated component with the cutoff reason."""
|
||||||
|
for member in members:
|
||||||
|
current = summaries_by_symbol[member.symbol]
|
||||||
|
summaries_by_symbol[member.symbol] = replace(
|
||||||
|
current,
|
||||||
|
completeness=join_completeness(current.completeness, Completeness(is_complete=False, reasons=(reason,))),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _stabilise_component(
|
def _stabilise_component(
|
||||||
members: tuple[FunctionSummary, ...],
|
members: tuple[FunctionSummary, ...],
|
||||||
summaries_by_symbol: dict[SymbolId, FunctionSummary],
|
summaries_by_symbol: dict[SymbolId, FunctionSummary],
|
||||||
edges: Mapping[SymbolId, tuple[SymbolId, ...]],
|
edges: Mapping[SymbolId, tuple[SymbolId, ...]],
|
||||||
|
budget: ExplorationBudget,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Iterate a component's members until every summary is stable."""
|
"""Iterate a component's members until stable or a budget bound fires."""
|
||||||
changed = True
|
if not any(edges.get(member.symbol) for member in members):
|
||||||
while changed:
|
return
|
||||||
changed = False
|
rounds = 0
|
||||||
for member in members:
|
while True:
|
||||||
current = summaries_by_symbol[member.symbol]
|
if budget.deadline is not None and monotonic() > budget.deadline:
|
||||||
callees = [summaries_by_symbol[edge] for edge in edges.get(member.symbol, ()) if edge in summaries_by_symbol]
|
_apply_cutoff_reason(members, summaries_by_symbol, _DEADLINE_CUTOFF)
|
||||||
propagated = _propagated(current, callees)
|
return
|
||||||
if propagated != current:
|
if not _propagate_round(members, summaries_by_symbol, edges, budget):
|
||||||
summaries_by_symbol[member.symbol] = propagated
|
return
|
||||||
changed = True
|
rounds += 1
|
||||||
|
if budget.depth is not None and rounds >= budget.depth:
|
||||||
|
_apply_cutoff_reason(members, summaries_by_symbol, _DEPTH_CUTOFF)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def monotone_summaries(
|
def monotone_summaries(
|
||||||
@@ -269,14 +349,18 @@ def monotone_summaries(
|
|||||||
*,
|
*,
|
||||||
operations_by_symbol: Mapping[SymbolId, tuple[Operation, ...]] | None = None,
|
operations_by_symbol: Mapping[SymbolId, tuple[Operation, ...]] | None = None,
|
||||||
witnesses_by_symbol: Mapping[SymbolId, tuple[Discharge, ...]] | None = None,
|
witnesses_by_symbol: Mapping[SymbolId, tuple[Discharge, ...]] | None = None,
|
||||||
|
budget: ExplorationBudget | None = None,
|
||||||
) -> tuple[FunctionSummary, ...]:
|
) -> tuple[FunctionSummary, ...]:
|
||||||
"""Summaries after the monotone fixpoint over strongly connected components.
|
"""Summaries after the monotone fixpoint over strongly connected components.
|
||||||
|
|
||||||
Local summaries first; then every component (callee components first) is
|
Local summaries first; then every component (callee components first) is
|
||||||
iterated until union-stable, so recursion terminates with the accumulated
|
iterated until union-stable, so recursion terminates with the accumulated
|
||||||
operations, witnesses and completeness reasons of its cycle. Output order
|
operations, witnesses and completeness reasons of its cycle. ``budget``
|
||||||
matches :func:`local_summaries`.
|
bounds the exploration (Req 3.8): a depth, fan-out or deadline cutoff
|
||||||
|
stops growth and leaves affected summaries incomplete with the exact
|
||||||
|
cutoff reason. Output order matches :func:`local_summaries`.
|
||||||
"""
|
"""
|
||||||
|
bounded = budget if budget is not None else ExplorationBudget()
|
||||||
local = local_summaries(
|
local = local_summaries(
|
||||||
functions_by_module,
|
functions_by_module,
|
||||||
index,
|
index,
|
||||||
@@ -287,5 +371,5 @@ def monotone_summaries(
|
|||||||
edges = _exact_caller_edges(summaries_by_symbol)
|
edges = _exact_caller_edges(summaries_by_symbol)
|
||||||
for component in _sorted_scc_components(edges, summaries_by_symbol):
|
for component in _sorted_scc_components(edges, summaries_by_symbol):
|
||||||
members = tuple(summaries_by_symbol[member] for member in component)
|
members = tuple(summaries_by_symbol[member] for member in component)
|
||||||
_stabilise_component(members, summaries_by_symbol, edges)
|
_stabilise_component(members, summaries_by_symbol, edges, bounded)
|
||||||
return tuple(summaries_by_symbol[summary.symbol] for summary in local)
|
return tuple(summaries_by_symbol[summary.symbol] for summary in local)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
from dataclasses import FrozenInstanceError
|
||||||
|
from time import monotonic
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -12,7 +14,12 @@ from openultrasast.semantic.obligations import summaries
|
|||||||
from openultrasast.semantic.obligations.abstract_domain import Completeness
|
from openultrasast.semantic.obligations.abstract_domain import Completeness
|
||||||
from openultrasast.semantic.obligations.operations import Discharge, Operation
|
from openultrasast.semantic.obligations.operations import Discharge, Operation
|
||||||
from openultrasast.semantic.obligations.resolution import SymbolId, SymbolIndex
|
from openultrasast.semantic.obligations.resolution import SymbolId, SymbolIndex
|
||||||
from openultrasast.semantic.obligations.summaries import FunctionSummary, local_summaries, monotone_summaries
|
from openultrasast.semantic.obligations.summaries import (
|
||||||
|
ExplorationBudget,
|
||||||
|
FunctionSummary,
|
||||||
|
local_summaries,
|
||||||
|
monotone_summaries,
|
||||||
|
)
|
||||||
|
|
||||||
PHP_CLASS = """<?php
|
PHP_CLASS = """<?php
|
||||||
class InvoiceController {
|
class InvoiceController {
|
||||||
@@ -347,3 +354,148 @@ def test_monotone_output_order_matches_local_summaries() -> None:
|
|||||||
assert [summary.symbol for summary in monotone_summaries(functions, index)] == [
|
assert [summary.symbol for summary in monotone_summaries(functions, index)] == [
|
||||||
summary.symbol for summary in local_summaries(functions, index)
|
summary.symbol for summary in local_summaries(functions, index)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
JAVA_FANOUT = """
|
||||||
|
class Fan {
|
||||||
|
void helper1() { this.helper2(); this.helper3(); }
|
||||||
|
void helper2() {}
|
||||||
|
void helper3() {}
|
||||||
|
}
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _java_operation(name: str, line: int) -> Operation:
|
||||||
|
return Operation(
|
||||||
|
path="src/C.java",
|
||||||
|
line=line,
|
||||||
|
function=name,
|
||||||
|
kind="protected_read",
|
||||||
|
fact_id=f"java:{name}-op",
|
||||||
|
resource=name,
|
||||||
|
requires=("path_guard",),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _fanout_functions() -> dict[str, tuple[FunctionIR, ...]]:
|
||||||
|
return {
|
||||||
|
"src/C.java": (
|
||||||
|
FunctionIR(
|
||||||
|
"helper1",
|
||||||
|
(),
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
(),
|
||||||
|
(_self_call("helper2", 3), _self_call("helper3", 3)),
|
||||||
|
),
|
||||||
|
FunctionIR("helper2", (), 4, 4, (), ()),
|
||||||
|
FunctionIR("helper3", (), 5, 5, (), ()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _mutual_functions() -> dict[str, tuple[FunctionIR, ...]]:
|
||||||
|
return {
|
||||||
|
"src/C.java": (
|
||||||
|
FunctionIR("a", (), 2, 4, (), (_self_call("b", 3),)),
|
||||||
|
FunctionIR("b", (), 5, 7, (), (_self_call("a", 6),)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _operations_injected(
|
||||||
|
functions: dict[str, tuple[FunctionIR, ...]], index: SymbolIndex, injected: dict[str, Operation]
|
||||||
|
) -> dict[SymbolId, tuple[Operation, ...]]:
|
||||||
|
operations_by_symbol: dict[SymbolId, tuple[Operation, ...]] = {}
|
||||||
|
for summary in local_summaries(functions, index):
|
||||||
|
operation = injected.get(summary.symbol.name)
|
||||||
|
if operation is not None:
|
||||||
|
operations_by_symbol[summary.symbol] = (operation,)
|
||||||
|
return operations_by_symbol
|
||||||
|
|
||||||
|
|
||||||
|
def test_depth_cutoff_preserves_accumulated_evidence_and_reason() -> None:
|
||||||
|
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
|
||||||
|
functions = _mutual_functions()
|
||||||
|
operations_by_symbol = _operations_injected(functions, index, {"b": _java_operation("b", 6)})
|
||||||
|
summaries_with_cutoff = monotone_summaries(
|
||||||
|
functions, index, operations_by_symbol=operations_by_symbol, budget=ExplorationBudget(depth=1)
|
||||||
|
)
|
||||||
|
summaries_unbounded = monotone_summaries(functions, index, operations_by_symbol=operations_by_symbol)
|
||||||
|
by_name = {summary.symbol.name: summary for summary in summaries_with_cutoff}
|
||||||
|
unbounded_by_name = {summary.symbol.name: summary for summary in summaries_unbounded}
|
||||||
|
assert by_name["a"].operations == unbounded_by_name["a"].operations
|
||||||
|
assert by_name["a"].operations == (_java_operation("b", 6),)
|
||||||
|
assert "summaries:depth_cutoff" in by_name["a"].completeness.reasons
|
||||||
|
assert "summaries:depth_cutoff" in by_name["b"].completeness.reasons
|
||||||
|
assert not by_name["a"].completeness.is_complete
|
||||||
|
|
||||||
|
|
||||||
|
def test_deadline_cutoff_returns_local_evidence() -> None:
|
||||||
|
index = _index((("src/C.java", JAVA_FANOUT, "java"),))
|
||||||
|
functions = _fanout_functions()
|
||||||
|
operations_by_symbol = _operations_injected(
|
||||||
|
functions, index, {"helper2": _java_operation("helper2", 4), "helper3": _java_operation("helper3", 5)}
|
||||||
|
)
|
||||||
|
summaries_with_cutoff = monotone_summaries(
|
||||||
|
functions, index, operations_by_symbol=operations_by_symbol, budget=ExplorationBudget(deadline=monotonic() - 1.0)
|
||||||
|
)
|
||||||
|
by_name = {summary.symbol.name: summary for summary in summaries_with_cutoff}
|
||||||
|
assert by_name["helper1"].operations == ()
|
||||||
|
assert "summaries:deadline_cutoff" in by_name["helper1"].completeness.reasons
|
||||||
|
assert not by_name["helper1"].completeness.is_complete
|
||||||
|
assert by_name["helper2"].completeness.is_complete
|
||||||
|
assert by_name["helper2"].operations == (_java_operation("helper2", 4),)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fan_out_cutoff_merges_sorted_prefix() -> None:
|
||||||
|
index = _index((("src/C.java", JAVA_FANOUT, "java"),))
|
||||||
|
functions = _fanout_functions()
|
||||||
|
operations_by_symbol = _operations_injected(
|
||||||
|
functions, index, {"helper2": _java_operation("helper2", 4), "helper3": _java_operation("helper3", 5)}
|
||||||
|
)
|
||||||
|
summaries_with_cutoff = monotone_summaries(
|
||||||
|
functions, index, operations_by_symbol=operations_by_symbol, budget=ExplorationBudget(fan_out=1)
|
||||||
|
)
|
||||||
|
by_name = {summary.symbol.name: summary for summary in summaries_with_cutoff}
|
||||||
|
assert by_name["helper1"].operations == (_java_operation("helper2", 4),)
|
||||||
|
assert "summaries:fan_out_cutoff" in by_name["helper1"].completeness.reasons
|
||||||
|
assert not by_name["helper1"].completeness.is_complete
|
||||||
|
assert by_name["helper3"].completeness.is_complete
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_none_budget_equals_default_behaviour() -> None:
|
||||||
|
index = _index((("src/C.java", JAVA_MUTUAL, "java"),))
|
||||||
|
functions = _mutual_functions()
|
||||||
|
operations_by_symbol = _operations_injected(functions, index, {"b": _java_operation("b", 6)})
|
||||||
|
with_default = monotone_summaries(functions, index, operations_by_symbol=operations_by_symbol)
|
||||||
|
with_empty_budget = monotone_summaries(functions, index, operations_by_symbol=operations_by_symbol, budget=ExplorationBudget())
|
||||||
|
assert with_default == with_empty_budget
|
||||||
|
|
||||||
|
|
||||||
|
def test_exploration_budget_is_frozen() -> None:
|
||||||
|
budget = ExplorationBudget(depth=1)
|
||||||
|
with pytest.raises(FrozenInstanceError):
|
||||||
|
budget.depth = 2 # type: ignore[misc]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cutoff_reasons_join_existing_incompleteness() -> None:
|
||||||
|
index = _index((("src/C.java", JAVA_FANOUT, "java"),))
|
||||||
|
functions = {
|
||||||
|
"src/C.java": (
|
||||||
|
FunctionIR(
|
||||||
|
"helper1",
|
||||||
|
(),
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
(),
|
||||||
|
(_self_call("helper2", 3), CallSite("missing", 3, (), (), (), False, 0)),
|
||||||
|
),
|
||||||
|
FunctionIR("helper2", (), 4, 4, (), ()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
summaries_with_cutoff = monotone_summaries(functions, index, budget=ExplorationBudget(deadline=monotonic() - 1.0))
|
||||||
|
by_name = {summary.symbol.name: summary for summary in summaries_with_cutoff}
|
||||||
|
reasons = by_name["helper1"].completeness.reasons
|
||||||
|
assert "resolution:no_candidate" in reasons
|
||||||
|
assert "summaries:deadline_cutoff" in reasons
|
||||||
|
|||||||
Reference in New Issue
Block a user