Skip to main content

fallow_engine/health/
test_reach.rs

1//! Test reachability for production-mode health runs.
2//!
3//! Production mode keeps test, story and dev files out of discovery, so the
4//! analysis graph has no test entry points. The static coverage estimate
5//! behind CRAP and coverage gaps then marks every function as untested. This
6//! module builds a graph of the full project, test files included, and maps
7//! the analysis file ids onto it. Findings still come from the production
8//! file set only.
9//!
10//! A health run builds this graph at most one time. File scoring and the
11//! runtime coverage signal index use the same graph.
12
13use std::path::Path;
14
15use fallow_config::ResolvedConfig;
16use rustc_hash::FxHashMap;
17
18use crate::discover::FileId;
19use crate::module_graph::{RetainedModuleGraph, StaticTestCoverage};
20use crate::session::AnalysisSession;
21
22/// Graph of the full project plus a map from analysis file ids to its ids.
23///
24/// The type is opaque. Downstream crates get it through the health seams and
25/// give it back to engine helpers such as
26/// [`crate::module_graph::module_value_exports_with_test_reach`].
27#[derive(Debug)]
28pub struct TestReachGraph {
29    graph: RetainedModuleGraph,
30    file_map: FxHashMap<FileId, FileId>,
31}
32
33impl TestReachGraph {
34    /// Build the graph when `config` is a production run. Returns `None`
35    /// outside production mode, and also when the full-project analysis
36    /// fails. In that case the caller keeps the analysis graph.
37    fn for_production(
38        config: &ResolvedConfig,
39        file_paths: &FxHashMap<FileId, &std::path::PathBuf>,
40    ) -> Option<Self> {
41        if !config.production {
42            return None;
43        }
44        let mut full_config = config.clone();
45        full_config.production = false;
46        let artifacts = AnalysisSession::from_resolved_config(full_config)
47            .and_then(|session| session.analyze_dead_code_retaining_files(false, true))
48            .inspect_err(|error| {
49                tracing::warn!(
50                    "cannot build the full-project graph for production test reachability: {error}"
51                );
52            })
53            .ok()?;
54        let graph = artifacts.graph?;
55        let files = artifacts.files?;
56        let ids_by_path: FxHashMap<&Path, FileId> = files
57            .iter()
58            .map(|file| (file.path.as_path(), file.id))
59            .collect();
60        let file_map = file_paths
61            .iter()
62            .filter_map(|(id, path)| {
63                ids_by_path
64                    .get(path.as_path())
65                    .map(|full_id| (*id, *full_id))
66            })
67            .collect();
68        Some(Self { graph, file_map })
69    }
70
71    pub(crate) fn static_test_coverage(&self) -> StaticTestCoverage<'_> {
72        StaticTestCoverage::mapped(self.graph.as_graph(), &self.file_map)
73    }
74}
75
76/// The test reachability graph of one health run, built on first use.
77///
78/// File scoring and the runtime coverage seam share one instance. The first
79/// call to [`SharedTestReach::get`] builds the graph, and later calls reuse
80/// it. A run that does not call `get` builds nothing. Both callers run while
81/// git churn loads on a different thread, so the build overlaps that load.
82pub struct SharedTestReach<'a> {
83    config: &'a ResolvedConfig,
84    file_paths: &'a FxHashMap<FileId, &'a std::path::PathBuf>,
85    graph: std::cell::OnceCell<Option<TestReachGraph>>,
86}
87
88impl<'a> SharedTestReach<'a> {
89    pub const fn new(
90        config: &'a ResolvedConfig,
91        file_paths: &'a FxHashMap<FileId, &'a std::path::PathBuf>,
92    ) -> Self {
93        Self {
94            config,
95            file_paths,
96            graph: std::cell::OnceCell::new(),
97        }
98    }
99
100    /// The graph, built on the first call. `None` outside production mode.
101    pub fn get(&self) -> Option<&TestReachGraph> {
102        self.graph
103            .get_or_init(|| TestReachGraph::for_production(self.config, self.file_paths))
104            .as_ref()
105    }
106}
107
108/// Build the set of export names that have at least one test-reachable reference.
109///
110/// This is the per-function signal: if an export named "foo" has a reference from
111/// a test-reachable module, the function "foo" is considered directly tested.
112pub(super) fn build_test_referenced_exports(
113    file_id: FileId,
114    exports: &[fallow_graph::graph::ExportSymbol],
115    test_coverage: StaticTestCoverage<'_>,
116) -> rustc_hash::FxHashSet<String> {
117    let mut set = rustc_hash::FxHashSet::default();
118    for export in exports {
119        if export.is_type_only {
120            continue;
121        }
122        let has_test_ref = test_coverage.covers_any_reference(file_id, export);
123        if has_test_ref {
124            set.insert(export.name.to_string());
125        }
126    }
127    set
128}