mod support;
use panicgraph::{
Category, CategorySet, FuncKey, Graph, Policy, Solver, witness,
};
use crate::support::{BodyBuilder, graph as build};
fn solve(
bodies: Vec<panicgraph::Body>,
suppressed: CategorySet,
) -> (Graph, panicgraph::Solution) {
solve_with(bodies, suppressed, panicgraph::solve::Edges::default())
}
fn solve_with(
bodies: Vec<panicgraph::Body>,
suppressed: CategorySet,
edges: panicgraph::solve::Edges,
) -> (Graph, panicgraph::Solution) {
let graph = build(bodies);
let policy = Policy { suppressed, edges };
let solution = Solver::new(&graph, policy)
.solve()
.expect("the solver should converge on a finite graph");
(graph, solution)
}
fn id(graph: &Graph, name: &str) -> panicgraph::FuncId {
graph
.id_of(&FuncKey(name.to_owned()))
.expect("the function should be in the graph")
}
#[test]
fn suppression_is_transitive() {
let bodies = vec![
BodyBuilder::new("caller").calls("grow").build(),
BodyBuilder::new("grow")
.panics(Category::CapacityOverflow)
.build(),
];
let (graph, solution) = solve(bodies.clone(), CategorySet::EMPTY);
assert!(
solution
.enabled(id(&graph, "caller"))
.contains(Category::CapacityOverflow),
"without suppression the caller inherits the allocation panic"
);
let (graph, solution) = solve(bodies, CategorySet::oom());
assert!(
solution.is_clean(id(&graph, "caller")),
"a caller that only panics through allocation is clean once \
allocation is suppressed"
);
}
#[test]
fn unrelated_panics_survive_suppression() {
let bodies = vec![
BodyBuilder::new("caller")
.calls("grow")
.calls("index")
.build(),
BodyBuilder::new("grow")
.panics(Category::CapacityOverflow)
.build(),
BodyBuilder::new("index").panics(Category::Index).build(),
];
let (graph, solution) = solve(bodies, CategorySet::oom());
let enabled = solution.enabled(id(&graph, "caller"));
assert!(
enabled.contains(Category::Index),
"suppressing allocation must not hide an index panic"
);
assert!(
!enabled.contains(Category::CapacityOverflow),
"the allocation panic is suppressed"
);
}
#[test]
fn cleanup_reachable_only_through_a_suppressed_panic_is_suppressed() {
let bodies = vec![
BodyBuilder::new("caller")
.calls("grow")
.calls_on_unwind_of("drop_glue", 0)
.build(),
BodyBuilder::new("grow")
.panics(Category::CapacityOverflow)
.build(),
BodyBuilder::new("drop_glue")
.panics(Category::Explicit)
.build(),
];
let (graph, solution) = solve(bodies.clone(), CategorySet::EMPTY);
assert!(
solution
.enabled(id(&graph, "caller"))
.contains(Category::Explicit),
"without suppression the unwind path reaches the panicking drop"
);
let (graph, solution) = solve(bodies, CategorySet::oom());
assert!(
solution.is_clean(id(&graph, "caller")),
"the drop is only reachable while the suppressed panic unwinds, so \
it is unreachable too"
);
}
#[test]
fn cleanup_reachable_normally_survives_suppression() {
let bodies = vec![
BodyBuilder::new("caller")
.calls("grow")
.calls("drop_glue")
.build(),
BodyBuilder::new("grow")
.panics(Category::CapacityOverflow)
.build(),
BodyBuilder::new("drop_glue")
.panics(Category::Explicit)
.build(),
];
let (graph, solution) = solve(bodies, CategorySet::oom());
assert!(
solution
.enabled(id(&graph, "caller"))
.contains(Category::Explicit),
"a drop on the normal path is unaffected by allocation suppression"
);
}
#[test]
fn recursion_converges() {
let bodies = vec![
BodyBuilder::new("a").calls("b").build(),
BodyBuilder::new("b")
.calls("a")
.panics(Category::Index)
.build(),
];
let (graph, solution) = solve(bodies, CategorySet::EMPTY);
assert!(
solution.enabled(id(&graph, "a")).contains(Category::Index),
"a cycle must still propagate the panic"
);
}
#[test]
fn missing_callees_are_unknown_not_clean() {
let bodies = vec![BodyBuilder::new("caller").calls("absent").build()];
let (graph, solution) = solve(bodies, CategorySet::EMPTY);
assert!(
solution
.enabled(id(&graph, "caller"))
.contains(Category::Unknown),
"a callee with no recorded body is unknown, never assumed clean"
);
}
#[test]
fn witness_names_the_panicking_function() {
let bodies = vec![
BodyBuilder::new("caller").calls("middle").build(),
BodyBuilder::new("middle").calls("leaf").build(),
BodyBuilder::new("leaf").panics(Category::Unwrap).build(),
];
let (graph, solution) = solve(bodies, CategorySet::EMPTY);
let path = witness::find(
&graph,
&solution,
id(&graph, "caller"),
Category::Unwrap,
)
.expect("the unwrap should be reachable");
assert_eq!(graph.body(path.func).display, "leaf");
assert_eq!(path.hops.len(), 2, "the path runs caller -> middle -> leaf");
assert_eq!(path.terminal, panicgraph::Terminal::Site(0));
}
#[test]
fn witness_is_absent_when_suppressed() {
let bodies = vec![
BodyBuilder::new("caller").calls("grow").build(),
BodyBuilder::new("grow")
.panics(Category::CapacityOverflow)
.build(),
];
let (graph, solution) = solve(bodies, CategorySet::oom());
assert!(
witness::find(
&graph,
&solution,
id(&graph, "caller"),
Category::CapacityOverflow,
)
.is_none(),
"a suppressed category has no witness"
);
}
#[test]
fn an_exact_name_wins_over_a_longer_one_that_contains_it() {
let graph = build(vec![
BodyBuilder::new("fold::{closure#0}").build(),
BodyBuilder::new("outer::fold::inner").build(),
BodyBuilder::new("fold").build(),
]);
let matches = graph.find_by_display("fold");
assert_eq!(matches.len(), 3);
assert_eq!(graph.body(matches[0]).display, "fold");
}
#[test]
fn a_barrier_contains_unwinding_panics_but_not_aborts() {
let (graph, solution) = solve(
vec![
BodyBuilder::new("callee")
.panics(Category::Explicit)
.aborts(Category::AllocFailure)
.build(),
BodyBuilder::new("caller")
.calls_behind_barrier("callee")
.build(),
],
CategorySet::EMPTY,
);
let caller = id(&graph, "caller");
assert!(
!solution.enabled(caller).contains(Category::Explicit),
"the unwinding panic must stop at the barrier"
);
assert!(
solution.enabled(caller).contains(Category::AllocFailure),
"an abort cannot be caught, so it must cross the barrier"
);
assert!(
!solution.unwinds(caller),
"a caller whose only panic aborts cannot unwind"
);
}
#[test]
fn cleanup_gated_on_a_barrier_call_stays_dead() {
let (graph, solution) = solve(
vec![
BodyBuilder::new("callee")
.panics(Category::Explicit)
.build(),
BodyBuilder::new("dropper").panics(Category::Index).build(),
BodyBuilder::new("caller")
.calls_behind_barrier("callee")
.calls_on_unwind_of("dropper", 0)
.build(),
],
CategorySet::EMPTY,
);
let caller = id(&graph, "caller");
assert!(
!solution.enabled(caller).contains(Category::Index),
"nothing unwinds out of a barrier call, so its cleanup cannot run"
);
}
#[test]
fn a_candidate_edge_is_followed_only_when_asked() {
let bodies = || {
vec![
BodyBuilder::new("one_impl").panics(Category::Index).build(),
BodyBuilder::new("caller")
.calls_candidate("one_impl")
.build(),
]
};
let (graph, solution) = solve(bodies(), CategorySet::EMPTY);
assert!(
!solution
.enabled(id(&graph, "caller"))
.contains(Category::Index),
"a candidate is not a proven target, so it stays unfollowed by \
default"
);
let (graph, solution) = solve_with(
bodies(),
CategorySet::EMPTY,
panicgraph::solve::Edges {
follow_inexact: true,
candidates: true,
},
);
assert!(
solution
.enabled(id(&graph, "caller"))
.contains(Category::Index),
"asking for candidates follows the edge"
);
}