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