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