panicgraph 0.2.1

Reports which functions can panic, why, and through what call path.
Documentation
//! Builders for the small graphs these tests run on.
//!
//! Every test binary compiles this module on its own, so a builder only some
//! of them reach for is not dead code in the usual sense.
#![allow(dead_code)]

use std::{
    path::{Path, PathBuf},
    process::{Command, Output},
    sync::{Mutex, PoisonError},
};

use panicgraph::{
    Artifact, Body, BuildConfig, CallSite, Category, EdgeKind, FuncKey, Graph,
    Guard, PanicSite, StdMode, Termination, UnwindOrigin,
};

/// Builds a function body one piece at a time.
pub struct BodyBuilder {
    body: Body,
}

impl BodyBuilder {
    /// Starts a local, non-opaque body.
    pub fn new(name: &str) -> Self {
        Self {
            body: Body {
                key: FuncKey(name.to_owned()),
                display: name.to_owned(),
                krate: "test".to_owned(),
                loc: None,
                sites: Vec::new(),
                calls: Vec::new(),
                opaque: false,
                foreign: false,
                local: true,
                from_tests: false,
            },
        }
    }

    /// Adds a panic raised on the ordinary control flow path.
    pub fn panics(mut self, category: Category) -> Self {
        self.body.sites.push(site(
            category,
            Termination::Unwind,
            Guard::always(),
        ));
        self
    }

    /// Adds a panic only when one is named, which is how a test asks for a
    /// function that cannot panic.
    pub fn maybe_panics(self, category: Option<Category>) -> Self {
        match category {
            Some(category) => self.panics(category),
            None => self,
        }
    }

    /// Adds a call on the ordinary control flow path.
    pub fn calls(mut self, callee: &str) -> Self {
        self.body.calls.push(call(callee, Guard::always()));
        self
    }

    /// Adds a call that is one possible target rather than the proven one.
    pub fn calls_candidate(mut self, callee: &str) -> Self {
        let mut edge = call(callee, Guard::always());
        edge.candidate = true;
        edge.kind = EdgeKind::Vtable;
        self.body.calls.push(edge);
        self
    }

    /// Adds a call whose target could not be resolved, of the given kind.
    pub fn calls_unresolved(mut self, kind: EdgeKind) -> Self {
        let mut edge = call("<unresolved>", Guard::always());
        edge.callee = None;
        edge.kind = kind;
        self.body.calls.push(edge);
        self
    }

    /// Adds a call whose unwinding panics are contained, as under a catch.
    pub fn calls_behind_barrier(mut self, callee: &str) -> Self {
        let mut edge = call(callee, Guard::always());
        edge.barrier = true;
        self.body.calls.push(edge);
        self
    }

    /// Adds an unwinding panic that aborts at the function's boundary.
    pub fn panics_without_leaving(mut self, category: Category) -> Self {
        let mut raised = site(category, Termination::Unwind, Guard::always());
        raised.terminates = true;
        self.body.sites.push(raised);
        self
    }

    /// Adds a call where unwinding out of the callee aborts.
    pub fn calls_without_unwinding(mut self, callee: &str) -> Self {
        let mut edge = call(callee, Guard::always());
        edge.terminates = true;
        self.body.calls.push(edge);
        self
    }

    /// Adds a panic that aborts rather than unwinds.
    pub fn aborts(mut self, category: Category) -> Self {
        self.body.sites.push(site(
            category,
            Termination::Abort,
            Guard::always(),
        ));
        self
    }

    /// Adds a call reachable only while the given earlier call unwinds.
    pub fn calls_on_unwind_of(mut self, callee: &str, call_index: u32) -> Self {
        self.body.calls.push(call(
            callee,
            Guard {
                normal: false,
                origins: vec![UnwindOrigin::Call(call_index)],
            },
        ));
        self
    }

    /// Finishes the body.
    pub fn build(self) -> Body {
        self.body
    }
}

/// A panic site with the given reachability.
fn site(
    category: Category,
    termination: Termination,
    guard: Guard,
) -> PanicSite {
    PanicSite {
        category,
        termination,
        reason: format!("{category} panic"),
        sink: None,
        loc: None,
        guard,
        certain: false,
        terminates: false,
    }
}

/// A statically resolved call with the given reachability.
fn call(callee: &str, guard: Guard) -> CallSite {
    CallSite {
        callee: Some(FuncKey(callee.to_owned())),
        callee_display: callee.to_owned(),
        kind: EdgeKind::Static,
        loc: None,
        guard,
        barrier: false,
        terminates: false,
        candidate: false,
        sig: None,
        self_ty: None,
    }
}

/// One crate's artifact holding the given bodies.
pub fn artifact(bodies: Vec<Body>) -> Artifact {
    Artifact {
        reified: Vec::new(),
        coerced: Vec::new(),
        krate: "test".to_owned(),
        source: None,
        test: false,
        config: BuildConfig {
            rustc: "test".to_owned(),
            profile: "release".to_owned(),
            debug_assertions: false,
            overflow_checks: false,
            std_mode: StdMode::Shipped,
            mir_opt_level: None,
        },
        bodies,
    }
}

/// Builds the graph a set of bodies makes, as one crate's artifact.
pub fn graph(bodies: Vec<Body>) -> Graph {
    Graph::from_artifacts(vec![artifact(bodies)])
}

/// Analyses the known fixture crate through the installed front end and
/// returns the categories reported per function.
///
/// Nothing is suppressed, so the answer is everything the analysis can see.
pub fn analyse_fixture(
    profile: &str,
    extra: &[&str],
) -> Vec<(String, Vec<String>)> {
    findings(&analyse_fixture_json(profile, extra))
}

/// The categories a json report gives each function.
pub fn findings(report: &serde_json::Value) -> Vec<(String, Vec<String>)> {
    let findings = report["findings"]
        .as_array()
        .expect("the report should list findings");
    findings
        .iter()
        .map(|finding| {
            let name = finding["function"].as_str().unwrap_or_default();
            let categories = finding["categories"]
                .as_array()
                .map(|list| {
                    list.iter()
                        .filter_map(|c| c.as_str().map(str::to_owned))
                        .collect()
                })
                .unwrap_or_default();
            (name.to_owned(), categories)
        })
        .collect()
}

/// Runs the front end on the known fixture crate, one run at a time.
pub fn run_on_fixture(args: &[&str]) -> Output {
    run_on(&fixture_dir(), args)
}

/// Runs the front end on a fixture crate, one run at a time.
///
/// A run with a new driver discards the fixture's build tree, which would
/// break a concurrent run still compiling into it.
pub fn run_on(dir: &Path, args: &[&str]) -> Output {
    static TURN: Mutex<()> = Mutex::new(());
    // A test that panicked while holding the lock left the tree intact.
    let _turn = TURN.lock().unwrap_or_else(PoisonError::into_inner);
    let exe = PathBuf::from(env!("CARGO_BIN_EXE_panicgraph"));
    Command::new(&exe)
        .arg("--manifest-dir")
        .arg(dir)
        .args(args)
        .output()
        .expect("the front end should run")
}

/// Analyses the known fixture crate and returns the raw json report.
pub fn analyse_fixture_json(
    profile: &str,
    extra: &[&str],
) -> serde_json::Value {
    analyse_json(&fixture_dir(), profile, extra)
}

/// Analyses a fixture crate with nothing suppressed and returns the raw
/// json report.
pub fn analyse_json(
    dir: &Path,
    profile: &str,
    extra: &[&str],
) -> serde_json::Value {
    let mut args = vec!["--profile", profile, "--suppress", "", "--json"];
    args.extend_from_slice(extra);
    let output = run_on(dir, &args);
    serde_json::from_slice(&output.stdout).unwrap_or_else(|err| {
        panic!(
            "the report should be json: {err}\n{}",
            String::from_utf8_lossy(&output.stderr)
        )
    })
}

/// Where the known fixture crate lives.
pub fn fixture_dir() -> PathBuf {
    fixture("known")
}

/// Where a fixture crate lives.
pub fn fixture(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join(name)
}