Skip to main content

supercov_engine/
rust_project.rs

1//! Cargo workspace discovery and isolated owned-Rust frontend preparation.
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    fs,
6    path::{Component, Path, PathBuf},
7    process::Command,
8};
9
10use ra_ap_syntax::{
11    AstNode, AstToken, Edition, SourceFile, SyntaxKind,
12    ast::{self, HasAttrs, HasModuleItem, HasName},
13};
14use serde::Deserialize;
15use sha2::{Digest, Sha256};
16
17use crate::{
18    coverage_report::CoverageManifest,
19    rust_instrumenter::{instrument_rust_file_numbered, test_code_path, test_item},
20    rust_runtime::render_rust_region_runtime,
21};
22
23const RUST_APPLICATION_SOURCE: &str = "Rust application source";
24/// A test, benchmark or example target, or a test module in a file of its
25/// own: read for where its assertions stand, and not measured.
26const RUST_TEST_CODE: &str = "test, benchmark or example code";
27/// Why a file the module tree reaches is not measured: a bench or an example
28/// `cargo test` does not build, or a module behind a `#[cfg]` that is off.
29pub const RUST_NOT_COMPILED: &str = "not compiled by this build";
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct PreparedRustProject {
33    pub workspace_root: PathBuf,
34    pub target_directory: PathBuf,
35    pub source_files: Vec<String>,
36    pub crate_roots: Vec<String>,
37    pub runtime_module: String,
38    pub manifest: CoverageManifest,
39    /// The probes' numbering: a region's slots, by ordinal.
40    pub probes: crate::rust_instrumenter::RustProbeLayout,
41    pub preparation: RustPreparationTimings,
42}
43
44/// Where preparing the instrumented workspace spent its time, for
45/// `SUPERCOV_PHASE_TIMING=1`.
46#[derive(Debug, Clone, Default, PartialEq)]
47pub struct RustPreparationTimings {
48    pub metadata_ms: f64,
49    pub discovery_ms: f64,
50    pub instrument_ms: f64,
51    pub runtime_ms: f64,
52}
53
54#[derive(Debug)]
55pub enum RustProjectError {
56    Io { path: PathBuf, reason: String },
57    MetadataLaunch(String),
58    MetadataFailed(String),
59    MetadataJson(String),
60    UnsafePath(String),
61    NoWorkspacePackages,
62    NoSourceFiles,
63    Instrument { file: String, reason: String },
64    DuplicateObligation(String),
65    Runtime(String),
66    SourceRoots(String),
67}
68
69impl std::fmt::Display for RustProjectError {
70    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
73            Self::MetadataLaunch(reason) => {
74                write!(formatter, "could not launch cargo metadata: {reason}")
75            }
76            Self::MetadataFailed(reason) => write!(formatter, "cargo metadata failed: {reason}"),
77            Self::MetadataJson(reason) => write!(formatter, "invalid cargo metadata: {reason}"),
78            Self::UnsafePath(path) => {
79                write!(formatter, "Cargo reported an unsafe workspace path: {path}")
80            }
81            Self::NoWorkspacePackages => {
82                write!(formatter, "Cargo metadata reported no workspace packages")
83            }
84            Self::NoSourceFiles => write!(
85                formatter,
86                "Cargo workspace contains no owned Rust source files"
87            ),
88            Self::Instrument { file, reason } => {
89                write!(formatter, "could not instrument {file}: {reason}")
90            }
91            Self::DuplicateObligation(id) => {
92                write!(formatter, "duplicate Rust obligation ID: {id}")
93            }
94            Self::Runtime(reason) => write!(formatter, "could not generate Rust runtime: {reason}"),
95            Self::SourceRoots(reason) => write!(formatter, "{reason}"),
96        }
97    }
98}
99
100impl std::error::Error for RustProjectError {}
101
102#[derive(Deserialize)]
103struct CargoMetadata {
104    packages: Vec<CargoPackage>,
105    workspace_members: Vec<String>,
106    workspace_root: PathBuf,
107    target_directory: PathBuf,
108}
109
110#[derive(Deserialize)]
111struct CargoPackage {
112    id: String,
113    manifest_path: PathBuf,
114    targets: Vec<CargoTarget>,
115}
116
117#[derive(Deserialize)]
118struct CargoTarget {
119    kind: Vec<String>,
120    src_path: PathBuf,
121}
122
123fn canonical_directory(path: &Path) -> Result<PathBuf, RustProjectError> {
124    fs::canonicalize(path).map_err(|error| RustProjectError::Io {
125        path: path.to_owned(),
126        reason: error.to_string(),
127    })
128}
129
130fn confined_relative(root: &Path, path: &Path) -> Result<String, RustProjectError> {
131    let relative = path
132        .strip_prefix(root)
133        .map_err(|_| RustProjectError::UnsafePath(path.display().to_string()))?;
134    if relative.as_os_str().is_empty()
135        || relative
136            .components()
137            .any(|component| !matches!(component, Component::Normal(_)))
138    {
139        return Err(RustProjectError::UnsafePath(path.display().to_string()));
140    }
141    Ok(relative.to_string_lossy().replace('\\', "/"))
142}
143
144fn cargo_metadata(root: &Path) -> Result<CargoMetadata, RustProjectError> {
145    let target_directory = root.join(".supercov/rust-target");
146    let output = Command::new("cargo")
147        .args(["metadata", "--format-version=1", "--no-deps"])
148        .current_dir(root)
149        .env("CARGO_TARGET_DIR", &target_directory)
150        .output()
151        .map_err(|error| RustProjectError::MetadataLaunch(error.to_string()))?;
152    if !output.status.success() {
153        return Err(RustProjectError::MetadataFailed(
154            String::from_utf8_lossy(&output.stderr).trim().to_owned(),
155        ));
156    }
157    serde_json::from_slice(&output.stdout)
158        .map_err(|error| RustProjectError::MetadataJson(error.to_string()))
159}
160
161/// The workspace sources the build actually read, from the depinfo rustc
162/// writes beside every artifact (`<artifact>.d`: a make rule whose
163/// prerequisites are every file that went into it). `None` when no depinfo
164/// could be read, which means "do not prune".
165///
166/// Source discovery follows `mod` declarations because that is what rustc
167/// RESOLVES; this is what it COMPILES. A module behind a `#[cfg]` that is off
168/// is resolved and never built, and its obligations can never be covered --
169/// smallvec's `serde`, `borsh`, `rayon` and `specialization` modules are 142
170/// such lines. Reading the build is exact for every `cfg` predicate, where
171/// evaluating them here would have to reproduce Cargo's feature resolution.
172pub fn compiled_source_files(
173    workspace: &Path,
174    target_directory: &Path,
175) -> Option<BTreeSet<String>> {
176    let mut compiled = BTreeSet::new();
177    let mut depinfo_files = 0;
178    let mut directories = vec![target_directory.to_path_buf()];
179    while let Some(directory) = directories.pop() {
180        let Ok(entries) = fs::read_dir(&directory) else {
181            continue;
182        };
183        for entry in entries.flatten() {
184            let path = entry.path();
185            match entry.file_type() {
186                Ok(file_type) if file_type.is_dir() => directories.push(path),
187                Ok(file_type) if file_type.is_file() => {
188                    if path.extension().is_some_and(|extension| extension == "d")
189                        && let Ok(text) = fs::read_to_string(&path)
190                    {
191                        depinfo_files += 1;
192                        collect_depinfo_sources(&text, workspace, &mut compiled);
193                    }
194                }
195                _ => {}
196            }
197        }
198    }
199    (depinfo_files > 0).then_some(compiled)
200}
201
202/// The prerequisites of every rule in a depinfo file, as workspace-relative
203/// paths. A rule is `target: prerequisite prerequisite`, and a space inside a
204/// path is escaped as `\ `.
205fn collect_depinfo_sources(text: &str, workspace: &Path, compiled: &mut BTreeSet<String>) {
206    for line in text.lines() {
207        // A prerequisite-only line (`path:` with nothing after it) carries no
208        // sources, and a target's own path is not a source.
209        let Some((_, prerequisites)) = line.split_once(": ") else {
210            continue;
211        };
212        let mut current = String::new();
213        let mut characters = prerequisites.chars().peekable();
214        while let Some(character) = characters.next() {
215            match character {
216                '\\' if characters.peek() == Some(&' ') => {
217                    characters.next();
218                    current.push(' ');
219                }
220                ' ' => {
221                    push_workspace_source(&current, workspace, compiled);
222                    current.clear();
223                }
224                _ => current.push(character),
225            }
226        }
227        push_workspace_source(&current, workspace, compiled);
228    }
229}
230
231fn push_workspace_source(path: &str, workspace: &Path, compiled: &mut BTreeSet<String>) {
232    let path = path.trim();
233    if path.is_empty() || !path.ends_with(".rs") {
234        return;
235    }
236    // Cargo runs rustc with the workspace as its working directory, so a
237    // source of the crate being built is named relatively and a dependency's
238    // source -- from the registry -- absolutely.
239    let candidate = Path::new(path);
240    let relative = if candidate.is_absolute() {
241        let Ok(relative) = candidate.strip_prefix(workspace) else {
242            return;
243        };
244        relative
245    } else {
246        candidate
247    };
248    if let Some(text) = relative.to_str() {
249        compiled.insert(text.replace('\\', "/"));
250    }
251}
252
253/// The files rustc compiles for the given crate roots: each root and,
254/// transitively, every module it declares with `mod name;` (resolved the way
255/// rustc resolves it, `#[path]` included) and every file it pulls in with a
256/// literal `include!("....rs")`. A `.rs` file under the package that no module
257/// reaches -- a runtime source embedded as data with `include_str!`, a test
258/// fixture, a snippet -- is not part of any crate, so instrumenting it would
259/// change the data and count code that is never compiled.
260///
261/// A file that does not exist is skipped, not an error: a `#[cfg]`-gated
262/// module may name a file the checkout lacks, and rustc only complains when
263/// that cfg is active. Files outside the workspace are left alone as well.
264/// Every file the crate roots reach. `tests` gets those only test code
265/// reaches -- through a test target's root or a `#[cfg(test)]` module --
266/// which are test code whatever their names. Every other route is followed
267/// first, so a file reached both ways is not test code. `owners` gets the
268/// crate roots that compile each file: one file can be in several crates.
269fn resolve_module_tree(
270    workspace: &Path,
271    roots: &BTreeSet<PathBuf>,
272    test_roots: &BTreeSet<PathBuf>,
273    files: &mut BTreeSet<PathBuf>,
274    tests: &mut BTreeSet<PathBuf>,
275    owners: &mut BTreeMap<PathBuf, BTreeSet<PathBuf>>,
276) -> Result<(), RustProjectError> {
277    let canonical_workspace = canonical_directory(workspace)?;
278    // (file, directory its `mod` children resolve in, crate root)
279    let (mut pending, mut pending_tests): (Vec<_>, Vec<_>) = roots
280        .iter()
281        .map(|root| (root.clone(), owner_directory(root), root.clone()))
282        .partition(|(root, _, _)| !test_roots.contains(root));
283    let mut visited = BTreeSet::new();
284    loop {
285        let (file, directory, root, test) = match pending.pop() {
286            Some((file, directory, root)) => (file, directory, root, false),
287            None => match pending_tests.pop() {
288                Some((file, directory, root)) => (file, directory, root, true),
289                None => break,
290            },
291        };
292        // `#[path = "../src/shared.rs"]` climbs out of its directory; the
293        // path is normalised lexically so the workspace check and the file
294        // set see one spelling of it.
295        let file = normalize(&file);
296        let directory = normalize(&directory);
297        if !file.starts_with(workspace) {
298            continue;
299        }
300        let Ok(metadata) = fs::symlink_metadata(&file) else {
301            continue;
302        };
303        // A symlink is followed only within the workspace: crossbeam shares
304        // one source file between its crates that way. The file is recorded
305        // under its target's path, so it is instrumented and digested once as
306        // a regular file; a symlink leaving the workspace would be
307        // instrumented in place, outside the copy, and is refused.
308        let file = if metadata.file_type().is_symlink() {
309            let target = fs::canonicalize(&file).map_err(|error| RustProjectError::Io {
310                path: file.clone(),
311                reason: error.to_string(),
312            })?;
313            if !target.starts_with(&canonical_workspace) || !target.is_file() {
314                return Err(RustProjectError::UnsafePath(file.display().to_string()));
315            }
316            target
317        } else if metadata.is_file() {
318            file.clone()
319        } else {
320            continue;
321        };
322        // A file is test code by the first route that reaches it; it is
323        // walked again for every crate that reaches it, to find its owners.
324        if files.insert(file.clone()) && test {
325            tests.insert(file.clone());
326        }
327        if !visited.insert((file.clone(), root.clone())) {
328            continue;
329        }
330        owners.entry(file.clone()).or_default().insert(root.clone());
331        let source = fs::read_to_string(&file).map_err(|error| RustProjectError::Io {
332            path: file.clone(),
333            reason: error.to_string(),
334        })?;
335        let parsed = SourceFile::parse(&source, Edition::CURRENT).tree();
336        let mut declared = Vec::new();
337        collect_module_declarations(
338            parsed.items(),
339            &file,
340            &directory,
341            false,
342            false,
343            &mut declared,
344        );
345        for (child, owner, gated) in declared {
346            if test || gated {
347                pending_tests.push((child, owner, root.clone()));
348            } else {
349                pending.push((child, owner, root.clone()));
350            }
351        }
352    }
353    Ok(())
354}
355
356/// Resolve `.` and `..` components without touching the filesystem.
357fn normalize(path: &Path) -> PathBuf {
358    let mut normalized = PathBuf::new();
359    for component in path.components() {
360        match component {
361            Component::ParentDir => {
362                normalized.pop();
363            }
364            Component::CurDir => {}
365            other => normalized.push(other.as_os_str()),
366        }
367    }
368    normalized
369}
370
371fn owner_directory(file: &Path) -> PathBuf {
372    file.parent().map_or_else(PathBuf::new, Path::to_path_buf)
373}
374
375/// Walk the items of one module body. `directory` is where this module's
376/// `mod name;` children live; `inline` says whether we are inside a
377/// `mod name { ... }` block, which changes what `#[path]` is relative to.
378/// `gated` says whether the body is test code, and is passed on with each
379/// declared file.
380fn collect_module_declarations(
381    items: impl Iterator<Item = ast::Item>,
382    file: &Path,
383    directory: &Path,
384    inline: bool,
385    gated: bool,
386    pending: &mut Vec<(PathBuf, PathBuf, bool)>,
387) {
388    for item in items {
389        match item {
390            ast::Item::Module(module) => {
391                let Some(name) = module.name() else {
392                    continue;
393                };
394                let gated = gated || test_item(module.syntax());
395                let name = name.text().to_string();
396                let path_attribute = module.attrs().find_map(|attr| {
397                    let is_path = attr
398                        .path()
399                        .is_some_and(|path| path.syntax().text() == "path");
400                    is_path.then(|| string_literal(attr.syntax())).flatten()
401                });
402                if let Some(list) = module.item_list() {
403                    let nested = directory.join(&name);
404                    collect_module_declarations(list.items(), file, &nested, true, gated, pending);
405                } else if let Some(path) = path_attribute {
406                    // Relative to the file's own directory at the top level,
407                    // to the inline module's directory inside a block; the
408                    // loaded file owns its directory like a `mod.rs` does.
409                    let base = if inline {
410                        directory.to_path_buf()
411                    } else {
412                        owner_directory(file)
413                    };
414                    let target = base.join(path);
415                    let owner = owner_directory(&target);
416                    pending.push((target, owner, gated));
417                } else {
418                    // `name.rs` and `name/mod.rs` both put their children in
419                    // `directory/name/`.
420                    let children = directory.join(&name);
421                    pending.push((
422                        directory.join(format!("{name}.rs")),
423                        children.clone(),
424                        gated,
425                    ));
426                    pending.push((children.join("mod.rs"), children, gated));
427                }
428            }
429            ast::Item::MacroCall(call) => {
430                let is_include = call.path().is_some_and(|path| {
431                    matches!(
432                        path.syntax().text().to_string().as_str(),
433                        "include" | "std::include" | "core::include" | "::std::include"
434                    )
435                });
436                if !is_include {
437                    // A macro that selects among implementations declares its
438                    // modules in its token tree, where no item walk reaches
439                    // them: hashbrown's `cfg_select!` and the `cfg_if!` in
440                    // crates everywhere. Both arms are followed; the build
441                    // decides which one counts.
442                    for name in token_tree_modules(call.syntax()) {
443                        let children = directory.join(&name);
444                        pending.push((
445                            directory.join(format!("{name}.rs")),
446                            children.clone(),
447                            gated,
448                        ));
449                        pending.push((children.join("mod.rs"), children, gated));
450                    }
451                    continue;
452                }
453                let Some(literal) = string_literal(call.syntax()) else {
454                    continue;
455                };
456                if !literal.ends_with(".rs") {
457                    continue;
458                }
459                // Included code is spliced into this module: its own `mod`
460                // declarations resolve where this module's do.
461                pending.push((
462                    owner_directory(file).join(literal),
463                    directory.to_path_buf(),
464                    gated,
465                ));
466            }
467            _ => {}
468        }
469    }
470}
471
472/// Every `mod <name>;` declared inside a macro's token tree. The tokens are
473/// unparsed there, so the sequence is matched directly.
474fn token_tree_modules(node: &ra_ap_syntax::SyntaxNode) -> Vec<String> {
475    let mut names = Vec::new();
476    let mut tokens = node
477        .descendants_with_tokens()
478        .filter_map(|element| element.into_token())
479        .filter(|token| !token.kind().is_trivia())
480        .peekable();
481    while let Some(token) = tokens.next() {
482        if token.text() != "mod" {
483            continue;
484        }
485        let Some(name) = tokens
486            .peek()
487            .filter(|next| next.kind() == SyntaxKind::IDENT)
488        else {
489            continue;
490        };
491        let name = name.text().to_string();
492        tokens.next();
493        // `mod name;` declares a file; `mod name { .. }` is inline and its
494        // items are already in this tree.
495        if tokens
496            .peek()
497            .is_some_and(|next| next.kind() == SyntaxKind::SEMICOLON)
498        {
499            tokens.next();
500            names.push(name);
501        }
502    }
503    names
504}
505
506/// The first string literal under a node, unescaped. Inside a macro's token
507/// tree the literal is a bare token, not a `Literal` node, so look at tokens.
508fn string_literal(node: &ra_ap_syntax::SyntaxNode) -> Option<String> {
509    node.descendants_with_tokens().find_map(|element| {
510        let string = ast::String::cast(element.into_token()?)?;
511        string.value().ok().map(|value| value.into_owned())
512    })
513}
514
515/// The crate roots of every workspace member: the source file of each Cargo
516/// target except build scripts, which Cargo compiles and runs on their own.
517/// The crate roots of proc-macro targets. What they compile to is a compiler
518/// plugin: rustc loads it while building the crate under test and runs it
519/// there, so no test process ever executes it and the owned probes see
520/// nothing.
521fn proc_macro_crate_roots(
522    workspace: &Path,
523    packages: &[CargoPackage],
524) -> Result<BTreeSet<PathBuf>, RustProjectError> {
525    let mut roots = BTreeSet::new();
526    for package in packages {
527        for target in &package.targets {
528            if !target.kind.iter().any(|kind| kind == "proc-macro") {
529                continue;
530            }
531            let root =
532                fs::canonicalize(&target.src_path).map_err(|error| RustProjectError::Io {
533                    path: target.src_path.clone(),
534                    reason: error.to_string(),
535                })?;
536            if confined_relative(workspace, &root).is_ok() {
537                roots.insert(root);
538            }
539        }
540    }
541    Ok(roots)
542}
543
544fn crate_roots(
545    workspace: &Path,
546    packages: &[CargoPackage],
547) -> Result<BTreeSet<PathBuf>, RustProjectError> {
548    let mut roots = BTreeSet::new();
549    for package in packages {
550        let directory = package.manifest_path.parent().ok_or_else(|| {
551            RustProjectError::UnsafePath(package.manifest_path.display().to_string())
552        })?;
553        let directory = canonical_directory(directory)?;
554        confined_relative(workspace, &directory).or_else(|error| {
555            (directory == workspace)
556                .then_some(String::new())
557                .ok_or(error)
558        })?;
559        for target in &package.targets {
560            if target.kind.iter().any(|kind| kind == "custom-build") {
561                continue;
562            }
563            let root =
564                fs::canonicalize(&target.src_path).map_err(|error| RustProjectError::Io {
565                    path: target.src_path.clone(),
566                    reason: error.to_string(),
567                })?;
568            confined_relative(workspace, &root)?;
569            roots.insert(root);
570        }
571    }
572    Ok(roots)
573}
574
575/// The crate roots of test, benchmark and example targets, whose module
576/// trees are test code.
577fn test_target_roots(packages: &[CargoPackage]) -> BTreeSet<PathBuf> {
578    packages
579        .iter()
580        .flat_map(|package| &package.targets)
581        .filter(|target| {
582            target
583                .kind
584                .iter()
585                .any(|kind| matches!(kind.as_str(), "test" | "bench" | "example"))
586        })
587        .filter_map(|target| fs::canonicalize(&target.src_path).ok())
588        .collect()
589}
590
591fn runtime_module_name(sources: &BTreeMap<String, String>) -> String {
592    let mut suffix = 0_usize;
593    loop {
594        let candidate = if suffix == 0 {
595            "__supercov_runtime_v1".to_owned()
596        } else {
597            format!("__supercov_runtime_v1_{suffix}")
598        };
599        if sources.values().all(|source| !source.contains(&candidate)) {
600            return candidate;
601        }
602        suffix += 1;
603    }
604}
605
606/// Twelve hex digits identifying an instrumentation: a digest of every
607/// obligation ID in the manifest. Two builds of the same sources share it;
608/// any other program's instrumentation, such as a fixture a test prepares
609/// and runs, has another.
610/// Obligations in a proc-macro crate leave the denominator: the compiler runs
611/// that code while it builds the crate under test, so a test process cannot
612/// observe it. They stay in the manifest -- the evidence files are named by a
613/// token over its obligation IDs -- and are declined, with one limitation per
614/// file saying why.
615fn decline_proc_macro_obligations(
616    workspace: &Path,
617    proc_macro_roots: &BTreeSet<PathBuf>,
618    manifest: &mut CoverageManifest,
619) -> Result<(), RustProjectError> {
620    if proc_macro_roots.is_empty() {
621        return Ok(());
622    }
623    let mut reached = BTreeSet::new();
624    resolve_module_tree(
625        workspace,
626        proc_macro_roots,
627        &BTreeSet::new(),
628        &mut reached,
629        &mut BTreeSet::new(),
630        &mut BTreeMap::new(),
631    )?;
632    let mut files = BTreeSet::new();
633    for path in reached {
634        if let Ok(relative) = confined_relative(workspace, &path) {
635            files.insert(relative);
636        }
637    }
638    if files.is_empty() {
639        return Ok(());
640    }
641    let mut unmeasured = manifest.unmeasured.iter().cloned().collect::<BTreeSet<_>>();
642    unmeasured.extend(
643        manifest
644            .points
645            .iter()
646            .filter(|point| files.contains(&point.file))
647            .map(|point| point.id.clone()),
648    );
649    unmeasured.extend(
650        manifest
651            .decisions
652            .iter()
653            .filter(|decision| files.contains(&decision.file))
654            .map(|decision| decision.id.clone()),
655    );
656    unmeasured.extend(
657        manifest
658            .branches
659            .iter()
660            .filter(|branch| files.contains(&branch.file))
661            .map(|branch| branch.id.clone()),
662    );
663    manifest.unmeasured = unmeasured.into_iter().collect();
664    // The boundaries already declared inside these files say nothing now.
665    manifest.limitations.retain(|limitation| {
666        limitation
667            .get("file")
668            .and_then(|file| file.as_str())
669            .is_none_or(|file| !files.contains(file))
670    });
671    for file in files {
672        manifest.limitations.push(serde_json::json!({
673            "id": format!("rust-proc-macro-runs-in-the-compiler#{file}"),
674            "kind": "source-scope",
675            "file": file,
676            "line": 1,
677            "column": 0,
678            "source": "",
679            "blocking": false,
680            "reason": "This crate compiles to a compiler plugin: rustc loads it and runs it while building the crate under test, so no test process executes it"
681        }));
682    }
683    Ok(())
684}
685
686pub fn manifest_token(manifest: &CoverageManifest) -> String {
687    let mut ids = manifest
688        .points
689        .iter()
690        .map(|point| point.id.as_str())
691        .chain(
692            manifest
693                .decisions
694                .iter()
695                .map(|decision| decision.id.as_str()),
696        )
697        .chain(manifest.branches.iter().flat_map(|branch| {
698            branch
699                .alternatives
700                .iter()
701                .map(|alternative| alternative.id.as_str())
702        }))
703        .collect::<Vec<_>>();
704    ids.sort_unstable();
705    ids.dedup();
706    let mut hasher = Sha256::new();
707    for id in ids {
708        hasher.update(id.as_bytes());
709        hasher.update(b"\n");
710    }
711    hex(&hasher.finalize()[..6])
712}
713
714/// The runtime names its evidence files `<crate key>-<pid>.events`; the key
715/// is the manifest token followed by a digest of the crate root, so the
716/// reader can tell this instrumentation's files from any other's and two
717/// crates of one process write separate files.
718fn crate_key(token: &str, path: &str) -> String {
719    format!("{token}{}", hex(&Sha256::digest(path.as_bytes())[..6]))
720}
721
722fn hex(bytes: &[u8]) -> String {
723    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
724}
725
726fn merge_manifest(
727    destination: &mut CoverageManifest,
728    mut source: CoverageManifest,
729) -> Result<(), RustProjectError> {
730    let mut ids = destination
731        .points
732        .iter()
733        .map(|point| point.id.as_str())
734        .chain(
735            destination
736                .decisions
737                .iter()
738                .map(|decision| decision.id.as_str()),
739        )
740        .chain(destination.branches.iter().map(|branch| branch.id.as_str()))
741        .collect::<BTreeSet<_>>();
742    for id in source
743        .points
744        .iter()
745        .map(|point| point.id.as_str())
746        .chain(source.decisions.iter().map(|decision| decision.id.as_str()))
747        .chain(source.branches.iter().map(|branch| branch.id.as_str()))
748    {
749        if !ids.insert(id) {
750            return Err(RustProjectError::DuplicateObligation(id.into()));
751        }
752    }
753    destination.points.append(&mut source.points);
754    destination
755        .assertion_sites
756        .append(&mut source.assertion_sites);
757    destination.decisions.append(&mut source.decisions);
758    destination.branches.append(&mut source.branches);
759    // Obligations the file declined -- a `const fn` body, a `GlobalAlloc`
760    // implementation -- are declined for the project too.
761    destination.unmeasured.append(&mut source.unmeasured);
762    // A limitation is one site in one file. Deduping on the id alone kept a
763    // single macro-expansion limitation for the whole project, charged to
764    // whichever file merged first.
765    let site = |value: &serde_json::Value| {
766        (
767            value
768                .get("id")
769                .and_then(|id| id.as_str())
770                .map(str::to_owned),
771            value
772                .get("file")
773                .and_then(|file| file.as_str())
774                .map(str::to_owned),
775            value.get("line").and_then(serde_json::Value::as_u64),
776            value.get("column").and_then(serde_json::Value::as_u64),
777        )
778    };
779    for limitation in source.limitations {
780        let key = site(&limitation);
781        if !destination
782            .limitations
783            .iter()
784            .any(|existing| site(existing) == key)
785        {
786            destination.limitations.push(limitation);
787        }
788    }
789    Ok(())
790}
791
792pub fn prepare_rust_project(
793    workspace: &Path,
794    roots: Option<&crate::source_discovery::ExplicitSourceRoots>,
795) -> Result<PreparedRustProject, RustProjectError> {
796    prepare_rust_project_for(workspace, roots, None)
797}
798
799/// `prepare_rust_project` for a build whose target cfg values are known:
800/// items its `#[cfg(..)]` leaves uncompiled are not measured.
801pub fn prepare_rust_project_for(
802    workspace: &Path,
803    roots: Option<&crate::source_discovery::ExplicitSourceRoots>,
804    cfg: Option<&crate::rust_instrumenter::TargetCfg>,
805) -> Result<PreparedRustProject, RustProjectError> {
806    // Named apart from the crate roots below, which reuse `roots`.
807    let source_roots = roots;
808    let elapsed = |started: std::time::Instant| started.elapsed().as_secs_f64() * 1000.0;
809    let mut preparation = RustPreparationTimings::default();
810    let workspace = canonical_directory(workspace)?;
811    let started = std::time::Instant::now();
812    let metadata = cargo_metadata(&workspace)?;
813    preparation.metadata_ms = elapsed(started);
814    let metadata_root = canonical_directory(&metadata.workspace_root)?;
815    if metadata_root != workspace {
816        return Err(RustProjectError::UnsafePath(
817            metadata.workspace_root.display().to_string(),
818        ));
819    }
820    let members = metadata
821        .workspace_members
822        .into_iter()
823        .collect::<BTreeSet<_>>();
824    let packages = metadata
825        .packages
826        .into_iter()
827        .filter(|package| members.contains(&package.id))
828        .collect::<Vec<_>>();
829    if packages.is_empty() {
830        return Err(RustProjectError::NoWorkspacePackages);
831    }
832
833    let started = std::time::Instant::now();
834    let roots = crate_roots(&workspace, &packages)?;
835    let proc_macro_roots = proc_macro_crate_roots(&workspace, &packages)?;
836    let mut files = BTreeSet::new();
837    let mut tests = BTreeSet::new();
838    let mut owners = BTreeMap::new();
839    resolve_module_tree(
840        &workspace,
841        &roots,
842        &test_target_roots(&packages),
843        &mut files,
844        &mut tests,
845        &mut owners,
846    )?;
847    // Which crates compile each file, by the name the gates carry.
848    let owners = owners
849        .into_iter()
850        .filter_map(|(path, roots)| {
851            confined_relative(&workspace, &path)
852                .ok()
853                .map(|relative| (relative, roots))
854        })
855        .collect::<BTreeMap<_, _>>();
856    // Test code is instrumented for its assertion markers alone.
857    let tests = tests
858        .iter()
859        .map(|path| confined_relative(&workspace, path))
860        .collect::<Result<BTreeSet<_>, _>>()?;
861    if files.is_empty() {
862        return Err(RustProjectError::NoSourceFiles);
863    }
864
865    let mut sources = BTreeMap::new();
866    for path in files {
867        let relative = confined_relative(&workspace, &path)?;
868        let source = fs::read_to_string(&path).map_err(|error| RustProjectError::Io {
869            path: path.clone(),
870            reason: error.to_string(),
871        })?;
872        sources.insert(relative, source);
873    }
874    preparation.discovery_ms = elapsed(started);
875    let started = std::time::Instant::now();
876    let runtime_module = runtime_module_name(&sources);
877    // Narrowed only after the runtime's name is chosen: that name must not
878    // collide with any module in the crate, including those the roots leave
879    // out. A module left out is simply not instrumented, and compiles as the
880    // author wrote it.
881    let mut outside = Vec::new();
882    if let Some(source_roots) = source_roots {
883        let mut kept = sources.keys().cloned().collect::<Vec<_>>();
884        source_roots.narrow(&mut kept, &mut outside, String::as_str);
885        let kept = kept.into_iter().collect::<BTreeSet<_>>();
886        sources.retain(|relative, _| kept.contains(relative));
887        source_roots
888            .refuse_if_empty(sources.len(), "Rust")
889            .map_err(RustProjectError::SourceRoots)?;
890    }
891    let runtime_path = format!("crate::{runtime_module}");
892    let mut manifest = CoverageManifest {
893        unmeasured: Vec::new(),
894        decisions: Vec::new(),
895        points: Vec::new(),
896        branches: Vec::new(),
897        limitations: Vec::new(),
898        scope: None,
899        assertion_sites: Vec::new(),
900    };
901    // One numbering spans the workspace: a file may be compiled into several
902    // crates, and every crate's runtime maps the same region.
903    let mut probes = crate::rust_instrumenter::RustProbeLayout::default();
904    for (relative, source) in &sources {
905        let test_file = tests.contains(relative) || test_code_path(relative);
906        let transformed = instrument_rust_file_numbered(
907            relative,
908            source,
909            &runtime_path,
910            test_file,
911            cfg,
912            &mut probes,
913        )
914        .map_err(|error| RustProjectError::Instrument {
915            file: relative.clone(),
916            reason: error.to_string(),
917        })?;
918        merge_manifest(&mut manifest, transformed.manifest)?;
919        fs::write(workspace.join(relative), transformed.code).map_err(|error| {
920            RustProjectError::Io {
921                path: workspace.join(relative),
922                reason: error.to_string(),
923            }
924        })?;
925    }
926
927    preparation.instrument_ms = elapsed(started);
928    let started = std::time::Instant::now();
929    decline_proc_macro_obligations(&workspace, &proc_macro_roots, &mut manifest)?;
930
931    let token = manifest_token(&manifest);
932    probes.token = token.clone();
933    let mut crate_roots = Vec::new();
934    let mut crate_files = Vec::new();
935    for root in roots {
936        let relative = confined_relative(&workspace, &root)?;
937        // The crate answers `cfg!` for the gates in the files it compiles.
938        let crate_cfgs = probes
939            .cfgs
940            .iter()
941            .enumerate()
942            .filter(|(_, gate)| {
943                owners
944                    .get(&gate.file)
945                    .is_some_and(|roots| roots.contains(&root))
946            })
947            .map(|(ordinal, _)| ordinal)
948            .collect::<Vec<_>>();
949        let key = crate_key(&token, &relative);
950        let runtime =
951            render_rust_region_runtime(&runtime_module, &token, &key, &probes, &crate_cfgs)
952                .map_err(RustProjectError::Runtime)?;
953        crate_files.push((
954            key,
955            owners
956                .iter()
957                .filter(|(_, roots)| roots.contains(&root))
958                .map(|(file, _)| file.clone())
959                .collect::<Vec<_>>(),
960        ));
961        let mut source = fs::read_to_string(&root).map_err(|error| RustProjectError::Io {
962            path: root.clone(),
963            reason: error.to_string(),
964        })?;
965        source.push('\n');
966        source.push_str(&runtime);
967        fs::write(&root, source).map_err(|error| RustProjectError::Io {
968            path: root,
969            reason: error.to_string(),
970        })?;
971        crate_roots.push(relative);
972    }
973    preparation.runtime_ms = elapsed(started);
974    probes.crates = crate_files.into_iter().collect();
975
976    manifest
977        .points
978        .sort_by(|left, right| left.id.cmp(&right.id));
979    manifest
980        .assertion_sites
981        .sort_by(|left, right| left.id.cmp(&right.id));
982    manifest
983        .decisions
984        .sort_by(|left, right| left.id.cmp(&right.id));
985    manifest
986        .branches
987        .sort_by(|left, right| left.id.cmp(&right.id));
988    manifest.limitations.sort_by(|left, right| {
989        left.get("id")
990            .and_then(|value| value.as_str())
991            .cmp(&right.get("id").and_then(|value| value.as_str()))
992    });
993    let target_directory = metadata.target_directory;
994    let target_directory = if target_directory.is_absolute() {
995        target_directory
996    } else {
997        workspace.join(target_directory)
998    };
999    if !target_directory.starts_with(&workspace) {
1000        return Err(RustProjectError::UnsafePath(
1001            target_directory.display().to_string(),
1002        ));
1003    }
1004    let kept = sources.keys().cloned().collect::<Vec<_>>();
1005    let scope = match source_roots {
1006        Some(source_roots) => source_roots
1007            .scope(&kept, &outside)
1008            .map_err(|error| RustProjectError::SourceRoots(error.to_string()))?,
1009        // What the build then does not compile is taken out where the build
1010        // is known: see `RUST_NOT_COMPILED`.
1011        None => {
1012            let (test_code, measured) = kept
1013                .into_iter()
1014                .partition::<Vec<_>, _>(|file| tests.contains(file) || test_code_path(file));
1015            crate::source_discovery::automatic_scope(
1016                measured
1017                    .into_iter()
1018                    .map(|file| (file, RUST_APPLICATION_SOURCE.to_owned())),
1019                test_code
1020                    .into_iter()
1021                    .map(|file| (file, RUST_TEST_CODE.to_owned())),
1022            )
1023        }
1024    };
1025    manifest.scope = Some(
1026        serde_json::to_value(scope)
1027            .map_err(|error| RustProjectError::SourceRoots(error.to_string()))?,
1028    );
1029    Ok(PreparedRustProject {
1030        workspace_root: workspace,
1031        target_directory,
1032        source_files: sources.into_keys().collect(),
1033        crate_roots,
1034        runtime_module,
1035        manifest,
1036        probes,
1037        preparation,
1038    })
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043    use std::{
1044        process::Command,
1045        sync::atomic::{AtomicU64, Ordering},
1046        time::{SystemTime, UNIX_EPOCH},
1047    };
1048
1049    use super::*;
1050
1051    fn fixture() -> PathBuf {
1052        // One test calls this today, so nothing can collide with it yet. The
1053        // counter is here because the clock is not enough on its own: it ticks
1054        // once per microsecond and every test shares the pid, so the second
1055        // test to use this helper would draw the same root as the first when
1056        // the two start together.
1057        static UNIQUE: AtomicU64 = AtomicU64::new(0);
1058        let nonce = SystemTime::now()
1059            .duration_since(UNIX_EPOCH)
1060            .unwrap()
1061            .as_nanos();
1062        let root = std::env::temp_dir().join(format!(
1063            "supercov-rust-project-{}-{nonce}-{}",
1064            std::process::id(),
1065            UNIQUE.fetch_add(1, Ordering::Relaxed)
1066        ));
1067        fs::create_dir(&root).unwrap();
1068        fs::create_dir(root.join("src")).unwrap();
1069        fs::create_dir(root.join("tests")).unwrap();
1070        fs::write(
1071            root.join("Cargo.toml"),
1072            "[package]\nname='rust-project-fixture'\nversion='0.0.0'\nedition='2024'\n",
1073        )
1074        .unwrap();
1075        fs::write(
1076            root.join("src/lib.rs"),
1077            r#"pub fn choose(first: bool, second: bool) -> i32 {
1078    if first && second { 7 } else { 3 }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    #[test]
1084    fn unit_choice() {
1085        assert_eq!(super::choose(true, true), 7);
1086    }
1087}
1088"#,
1089        )
1090        .unwrap();
1091        fs::write(
1092            root.join("tests/integration.rs"),
1093            r#"#[test]
1094fn integration_choice() {
1095    assert_eq!(rust_project_fixture::choose(false, true), 3);
1096}
1097"#,
1098        )
1099        .unwrap();
1100        root
1101    }
1102
1103    #[test]
1104    fn explicit_roots_narrow_what_rust_instruments() {
1105        // A module the roots leave out is not instrumented, so it compiles as
1106        // the author wrote it. The runtime module is still declared in the
1107        // crate root whether or not that root is kept, which is what lets the
1108        // modules that are kept refer to it.
1109        let root = fixture();
1110        fs::write(
1111            root.join("Cargo.toml"),
1112            "[package]\nname='rust_project_fixture'\nversion='0.0.0'\nedition='2024'\n",
1113        )
1114        .unwrap();
1115        fs::write(
1116            root.join("src/lib.rs"),
1117            "mod helper;\nmod vendored;\npub fn entry(flag: bool) -> i32 { if flag { helper::one() } else { vendored::two() } }\n",
1118        )
1119        .unwrap();
1120        fs::write(root.join("src/helper.rs"), "pub fn one() -> i32 { 1 }\n").unwrap();
1121        let vendored = "pub fn two() -> i32 { 2 }\n";
1122        fs::write(root.join("src/vendored.rs"), vendored).unwrap();
1123
1124        let roots = crate::source_discovery::ExplicitSourceRoots::resolve(
1125            &root,
1126            &["src/lib.rs".to_owned(), "src/helper.rs".to_owned()],
1127        )
1128        .unwrap();
1129        let prepared = prepare_rust_project(&root, Some(&roots)).unwrap();
1130        assert_eq!(prepared.source_files, ["src/helper.rs", "src/lib.rs"]);
1131        assert!(
1132            prepared
1133                .manifest
1134                .points
1135                .iter()
1136                .all(|point| point.file != "src/vendored.rs"),
1137            "a module outside the roots carries no obligations"
1138        );
1139        assert_eq!(
1140            fs::read_to_string(root.join("src/vendored.rs")).unwrap(),
1141            vendored,
1142            "and is left exactly as written"
1143        );
1144        let scope: crate::source_discovery::SourceScope =
1145            serde_json::from_value(prepared.manifest.scope.clone().expect("a scope")).unwrap();
1146        assert_eq!(
1147            scope.mode,
1148            crate::source_discovery::SourceScopeMode::Explicit
1149        );
1150        assert!(
1151            scope
1152                .entries
1153                .iter()
1154                .any(|entry| entry.file == "src/vendored.rs"
1155                    && entry.reason == "outside explicit source roots"),
1156            "{scope:?}"
1157        );
1158        fs::remove_dir_all(root).ok();
1159    }
1160
1161    #[test]
1162    fn test_code_is_instrumented_for_its_assertions_alone() {
1163        // An integration test, a `#[cfg(test)]` module inline and one in its
1164        // own file, named like any other: none of it is measured, as
1165        // the usual Rust coverage tools leave it out. Each assertion still
1166        // says where it stood.
1167        let root = fixture();
1168        let library = fs::read_to_string(root.join("src/lib.rs")).unwrap();
1169        fs::write(
1170            root.join("src/lib.rs"),
1171            format!("#[cfg(test)]\nmod helpers;\n{library}"),
1172        )
1173        .unwrap();
1174        fs::write(
1175            root.join("src/helpers.rs"),
1176            "pub fn twice(value: i32) -> i32 {\n    let doubled = value * 2;\n    assert!(doubled >= value);\n    doubled\n}\n",
1177        )
1178        .unwrap();
1179
1180        let prepared = prepare_rust_project(&root, None).unwrap();
1181        assert_eq!(
1182            prepared.source_files,
1183            ["src/helpers.rs", "src/lib.rs", "tests/integration.rs"]
1184        );
1185        let manifest = &prepared.manifest;
1186        let measured = manifest
1187            .points
1188            .iter()
1189            .map(|point| (point.file.as_str(), point.line))
1190            .chain(
1191                manifest
1192                    .decisions
1193                    .iter()
1194                    .map(|decision| (decision.file.as_str(), decision.line)),
1195            )
1196            .chain(
1197                manifest
1198                    .branches
1199                    .iter()
1200                    .map(|branch| (branch.file.as_str(), branch.line)),
1201            )
1202            .collect::<BTreeSet<_>>();
1203        // `choose` alone: its function, its statement, its decision.
1204        assert!(
1205            measured
1206                .iter()
1207                .all(|(file, line)| *file == "src/lib.rs" && (3..=5).contains(line)),
1208            "{measured:?}"
1209        );
1210        assert!(!measured.is_empty());
1211        let sites = manifest
1212            .assertion_sites
1213            .iter()
1214            .map(|site| (site.file.as_str(), site.source.as_str()))
1215            .collect::<BTreeSet<_>>();
1216        assert_eq!(
1217            sites,
1218            BTreeSet::from([
1219                ("src/helpers.rs", "assert!(doubled >= value);"),
1220                ("src/lib.rs", "assert_eq!(super::choose(true, true), 7);"),
1221                (
1222                    "tests/integration.rs",
1223                    "assert_eq!(rust_project_fixture::choose(false, true), 3);"
1224                ),
1225            ])
1226        );
1227        // Test code keeps its markers and nothing else.
1228        let helpers = fs::read_to_string(root.join("src/helpers.rs")).unwrap();
1229        assert!(helpers.contains("::assertion("), "{helpers}");
1230        assert!(!helpers.contains("::hit("), "{helpers}");
1231        fs::remove_dir_all(&root).ok();
1232    }
1233
1234    #[test]
1235    fn a_proc_macro_crates_own_code_is_declined() {
1236        // async-trait, serde_derive and thiserror-impl compile to compiler
1237        // plugins: rustc loads them while building the crate under test and
1238        // runs them there, so no test process executes a line of them and
1239        // every file read 0%.
1240        let root = fixture();
1241        fs::write(
1242            root.join("Cargo.toml"),
1243            concat!(
1244                "[package]\nname='rust_project_fixture'\nversion='0.0.0'\nedition='2024'\n",
1245                "\n[lib]\nproc-macro=true\n",
1246            ),
1247        )
1248        .unwrap();
1249        fs::write(
1250            root.join("src/lib.rs"),
1251            "mod helper;\npub fn entry(flag: bool) -> i32 { if flag { helper::one() } else { 0 } }\n",
1252        )
1253        .unwrap();
1254        fs::write(root.join("src/helper.rs"), "pub fn one() -> i32 { 1 }\n").unwrap();
1255
1256        let prepared = prepare_rust_project(&root, None).unwrap();
1257        // The files are still instrumented and still in the manifest; their
1258        // obligations are declined, and a limitation says why.
1259        let declined = prepared.manifest.unmeasured.iter().collect::<BTreeSet<_>>();
1260        assert!(!declined.is_empty());
1261        for point in &prepared.manifest.points {
1262            let plugin = point.file == "src/lib.rs" || point.file == "src/helper.rs";
1263            assert_eq!(declined.contains(&point.id), plugin, "{}", point.file);
1264        }
1265        let reasons = prepared
1266            .manifest
1267            .limitations
1268            .iter()
1269            .filter_map(|limitation| limitation.get("id")?.as_str())
1270            .filter(|id| id.starts_with("rust-proc-macro-runs-in-the-compiler#"))
1271            .collect::<BTreeSet<_>>();
1272        assert_eq!(
1273            reasons,
1274            BTreeSet::from([
1275                "rust-proc-macro-runs-in-the-compiler#src/helper.rs",
1276                "rust-proc-macro-runs-in-the-compiler#src/lib.rs",
1277            ])
1278        );
1279        fs::remove_dir_all(&root).ok();
1280    }
1281
1282    #[test]
1283    fn modules_declared_inside_a_macro_are_instrumented() {
1284        // hashbrown selects its SIMD group with `cfg_select! { ... mod neon;
1285        // ... }`, and `cfg_if!` has the same shape; the declaration lives in
1286        // the macro's token tree, which no item walk reaches. Both arms are
1287        // followed -- the build decides which one is in the denominator.
1288        let root = fixture();
1289        fs::write(
1290            root.join("src/lib.rs"),
1291            concat!(
1292                "macro_rules! select { ($($rest:tt)*) => { $($rest)* } }\n",
1293                "select! {\n",
1294                "    #[cfg(target_endian = \"little\")]\n",
1295                "    mod little;\n",
1296                "    #[cfg(not(target_endian = \"little\"))]\n",
1297                "    mod big;\n",
1298                "    mod inline { pub fn here() -> i32 { 1 } }\n",
1299                "}\n",
1300                "pub fn value() -> i32 { inline::here() }\n",
1301            ),
1302        )
1303        .unwrap();
1304        fs::write(root.join("src/little.rs"), "pub fn v() -> i32 { 1 }\n").unwrap();
1305        fs::write(root.join("src/big.rs"), "pub fn v() -> i32 { 2 }\n").unwrap();
1306
1307        let prepared = prepare_rust_project(&root, None).unwrap();
1308        // Both arms, and never the inline module, which has no file.
1309        assert_eq!(
1310            prepared.source_files,
1311            [
1312                "src/big.rs",
1313                "src/lib.rs",
1314                "src/little.rs",
1315                "tests/integration.rs"
1316            ],
1317            "{:?}",
1318            prepared.source_files
1319        );
1320        fs::remove_dir_all(&root).ok();
1321    }
1322
1323    #[test]
1324    fn only_files_the_module_tree_reaches_are_instrumented() {
1325        let root = fixture();
1326        fs::create_dir_all(root.join("src/nested")).unwrap();
1327        fs::create_dir_all(root.join("src/deep/inner")).unwrap();
1328        fs::create_dir_all(root.join("runtime-assets")).unwrap();
1329        fs::write(
1330            root.join("src/lib.rs"),
1331            concat!(
1332                "mod util;\n",
1333                "mod nested;\n",
1334                "#[path = \"renamed_file.rs\"]\n",
1335                "mod renamed;\n",
1336                "mod deep;\n",
1337                "include!(\"included.rs\");\n",
1338                "pub const EMBEDDED: &str = include_str!(\"../runtime-assets/embedded.rs\");\n",
1339                "pub fn choose(first: bool, second: bool) -> i32 {\n",
1340                "    if first && second { util::seven() } else { nested::three() }\n",
1341                "}\n",
1342            ),
1343        )
1344        .unwrap();
1345        fs::write(root.join("src/util.rs"), "pub fn seven() -> i32 { 7 }\n").unwrap();
1346        fs::write(
1347            root.join("src/nested/mod.rs"),
1348            "mod leaf;\npub fn three() -> i32 { leaf::three() }\n",
1349        )
1350        .unwrap();
1351        fs::write(
1352            root.join("src/nested/leaf.rs"),
1353            "pub fn three() -> i32 { 3 }\n",
1354        )
1355        .unwrap();
1356        fs::write(
1357            root.join("src/renamed_file.rs"),
1358            "pub fn renamed() -> i32 { 1 }\n",
1359        )
1360        .unwrap();
1361        fs::write(
1362            root.join("src/deep.rs"),
1363            "pub mod inner {\n    mod block_child;\n    pub fn deep() -> i32 { block_child::v() }\n}\n",
1364        )
1365        .unwrap();
1366        fs::write(
1367            root.join("src/deep/inner/block_child.rs"),
1368            "pub fn v() -> i32 { 9 }\n",
1369        )
1370        .unwrap();
1371        fs::write(
1372            root.join("src/included.rs"),
1373            "pub fn included() -> i32 { 2 }\n",
1374        )
1375        .unwrap();
1376        // serde_json's tests reach into src with `#[path = "../src/..."]`.
1377        fs::write(
1378            root.join("tests/integration.rs"),
1379            concat!(
1380                "#[path = \"../src/util.rs\"]\n",
1381                "mod util;\n",
1382                "#[test]\n",
1383                "fn integration_choice() {\n",
1384                "    assert_eq!(rust_project_fixture::choose(false, true), 3);\n",
1385                "    assert_eq!(util::seven(), 7);\n",
1386                "}\n",
1387            ),
1388        )
1389        .unwrap();
1390        // Data, not code: embedded verbatim and compiled by a consumer of
1391        // its own, which would not know any runtime module of ours.
1392        let embedded = "pub fn standalone() -> i32 { if true { 1 } else { 0 } }\n";
1393        fs::write(root.join("runtime-assets/embedded.rs"), embedded).unwrap();
1394        fs::write(
1395            root.join("src/orphan.rs"),
1396            "pub fn unreachable_module() {}\n",
1397        )
1398        .unwrap();
1399
1400        let prepared = prepare_rust_project(&root, None).unwrap();
1401        assert_eq!(
1402            prepared.source_files,
1403            [
1404                "src/deep.rs",
1405                "src/deep/inner/block_child.rs",
1406                "src/included.rs",
1407                "src/lib.rs",
1408                "src/nested/leaf.rs",
1409                "src/nested/mod.rs",
1410                "src/renamed_file.rs",
1411                "src/util.rs",
1412                "tests/integration.rs",
1413            ]
1414        );
1415        assert_eq!(
1416            fs::read_to_string(root.join("runtime-assets/embedded.rs")).unwrap(),
1417            embedded
1418        );
1419        assert!(
1420            !fs::read_to_string(root.join("src/orphan.rs"))
1421                .unwrap()
1422                .contains("__supercov")
1423        );
1424        assert!(
1425            fs::read_to_string(root.join("src/deep/inner/block_child.rs"))
1426                .unwrap()
1427                .contains("__supercov")
1428        );
1429        let build = Command::new("cargo")
1430            .args(["test", "--no-run"])
1431            .current_dir(&root)
1432            .env("CARGO_TARGET_DIR", &prepared.target_directory)
1433            .output()
1434            .unwrap();
1435        assert!(
1436            build.status.success(),
1437            "{}",
1438            String::from_utf8_lossy(&build.stderr)
1439        );
1440        fs::remove_dir_all(root).unwrap();
1441    }
1442
1443    #[cfg(unix)]
1444    #[test]
1445    fn a_module_shared_through_a_symlink_is_instrumented_once() {
1446        let root = fixture();
1447        fs::write(root.join("src/shared.rs"), "pub fn shared() -> i32 { 5 }\n").unwrap();
1448        std::os::unix::fs::symlink("../src/shared.rs", root.join("tests/shared.rs")).unwrap();
1449        fs::write(
1450            root.join("src/lib.rs"),
1451            concat!(
1452                "pub mod shared;\n",
1453                "pub fn choose(first: bool, second: bool) -> i32 {\n",
1454                "    if first && second { 7 } else { shared::shared() }\n",
1455                "}\n",
1456            ),
1457        )
1458        .unwrap();
1459        fs::write(
1460            root.join("tests/integration.rs"),
1461            concat!(
1462                "mod shared;\n",
1463                "#[test]\n",
1464                "fn integration_choice() {\n",
1465                "    assert_eq!(rust_project_fixture::choose(false, true), 5);\n",
1466                "    assert_eq!(shared::shared(), 5);\n",
1467                "}\n",
1468            ),
1469        )
1470        .unwrap();
1471        let prepared = prepare_rust_project(&root, None).unwrap();
1472        // The target's path, once; never the symlink's spelling.
1473        let shared = prepared
1474            .source_files
1475            .iter()
1476            .filter(|file| file.ends_with("shared.rs"))
1477            .collect::<Vec<_>>();
1478        assert_eq!(shared, ["src/shared.rs"], "{:?}", prepared.source_files);
1479        // The one function in it carries one function probe: instrumented
1480        // once, through whichever spelling reached it first.
1481        let instrumented = fs::read_to_string(root.join("src/shared.rs")).unwrap();
1482        let function = prepared
1483            .manifest
1484            .points
1485            .iter()
1486            .find(|point| point.file == "src/shared.rs" && point.id.starts_with("rs:function:"))
1487            .expect("the shared function's probe");
1488        let ordinal = prepared
1489            .probes
1490            .hits
1491            .iter()
1492            .position(|id| id == &function.id)
1493            .unwrap();
1494        assert_eq!(
1495            instrumented.matches(&format!("::hit({ordinal});")).count(),
1496            1
1497        );
1498        let build = Command::new("cargo")
1499            .args(["test", "--no-run"])
1500            .current_dir(&root)
1501            .env("CARGO_TARGET_DIR", &prepared.target_directory)
1502            .output()
1503            .unwrap();
1504        assert!(
1505            build.status.success(),
1506            "{}",
1507            String::from_utf8_lossy(&build.stderr)
1508        );
1509        fs::remove_dir_all(root).unwrap();
1510    }
1511
1512    #[test]
1513    fn crate_keys_carry_the_manifest_token() {
1514        let root = fixture();
1515        let prepared = prepare_rust_project(&root, None).unwrap();
1516        let token = manifest_token(&prepared.manifest);
1517        assert_eq!(token.len(), 12);
1518        assert!(token.bytes().all(|byte| byte.is_ascii_hexdigit()));
1519        assert_eq!(token, manifest_token(&prepared.manifest));
1520        let key = crate_key(&token, "src/lib.rs");
1521        assert_eq!(key.len(), 24);
1522        assert!(key.starts_with(&token));
1523        assert_ne!(key, crate_key(&token, "tests/integration.rs"));
1524        for crate_root in &prepared.crate_roots {
1525            assert!(
1526                fs::read_to_string(root.join(crate_root))
1527                    .unwrap()
1528                    .contains(&crate_key(&token, crate_root))
1529            );
1530        }
1531        fs::remove_dir_all(root).unwrap();
1532    }
1533
1534    #[test]
1535    fn prepares_every_workspace_crate_root_and_compiles_without_manifest_changes() {
1536        let root = fixture();
1537        let manifest_before = fs::read(root.join("Cargo.toml")).unwrap();
1538        let prepared = prepare_rust_project(&root, None).unwrap();
1539        assert_eq!(
1540            prepared.source_files,
1541            ["src/lib.rs", "tests/integration.rs"]
1542        );
1543        assert_eq!(prepared.crate_roots, ["src/lib.rs", "tests/integration.rs"]);
1544        assert!(!prepared.manifest.points.is_empty());
1545        assert!(!prepared.manifest.decisions.is_empty());
1546        assert_eq!(fs::read(root.join("Cargo.toml")).unwrap(), manifest_before);
1547        for crate_root in &prepared.crate_roots {
1548            assert!(
1549                fs::read_to_string(root.join(crate_root))
1550                    .unwrap()
1551                    .contains(&format!("mod {}", prepared.runtime_module))
1552            );
1553        }
1554        let build = Command::new("cargo")
1555            .args(["test", "--no-run"])
1556            .current_dir(&root)
1557            .env("CARGO_TARGET_DIR", &prepared.target_directory)
1558            .output()
1559            .unwrap();
1560        assert!(
1561            build.status.success(),
1562            "{}",
1563            String::from_utf8_lossy(&build.stderr)
1564        );
1565        fs::remove_dir_all(root).unwrap();
1566    }
1567}