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,
12    ast::{self, HasAttrs, HasModuleItem, HasName},
13};
14use serde::Deserialize;
15use sha2::{Digest, Sha256};
16
17use crate::{
18    coverage_report::CoverageManifest, rust_instrumenter::instrument_rust_source,
19    rust_runtime::render_rust_runtime,
20};
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct PreparedRustProject {
24    pub workspace_root: PathBuf,
25    pub target_directory: PathBuf,
26    pub source_files: Vec<String>,
27    pub crate_roots: Vec<String>,
28    pub runtime_module: String,
29    pub manifest: CoverageManifest,
30}
31
32#[derive(Debug)]
33pub enum RustProjectError {
34    Io { path: PathBuf, reason: String },
35    MetadataLaunch(String),
36    MetadataFailed(String),
37    MetadataJson(String),
38    UnsafePath(String),
39    NoWorkspacePackages,
40    NoSourceFiles,
41    Instrument { file: String, reason: String },
42    DuplicateObligation(String),
43    Runtime(String),
44}
45
46impl std::fmt::Display for RustProjectError {
47    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
50            Self::MetadataLaunch(reason) => {
51                write!(formatter, "could not launch cargo metadata: {reason}")
52            }
53            Self::MetadataFailed(reason) => write!(formatter, "cargo metadata failed: {reason}"),
54            Self::MetadataJson(reason) => write!(formatter, "invalid cargo metadata: {reason}"),
55            Self::UnsafePath(path) => {
56                write!(formatter, "Cargo reported an unsafe workspace path: {path}")
57            }
58            Self::NoWorkspacePackages => {
59                write!(formatter, "Cargo metadata reported no workspace packages")
60            }
61            Self::NoSourceFiles => write!(
62                formatter,
63                "Cargo workspace contains no owned Rust source files"
64            ),
65            Self::Instrument { file, reason } => {
66                write!(formatter, "could not instrument {file}: {reason}")
67            }
68            Self::DuplicateObligation(id) => {
69                write!(formatter, "duplicate Rust obligation ID: {id}")
70            }
71            Self::Runtime(reason) => write!(formatter, "could not generate Rust runtime: {reason}"),
72        }
73    }
74}
75
76impl std::error::Error for RustProjectError {}
77
78#[derive(Deserialize)]
79struct CargoMetadata {
80    packages: Vec<CargoPackage>,
81    workspace_members: Vec<String>,
82    workspace_root: PathBuf,
83    target_directory: PathBuf,
84}
85
86#[derive(Deserialize)]
87struct CargoPackage {
88    id: String,
89    manifest_path: PathBuf,
90    targets: Vec<CargoTarget>,
91}
92
93#[derive(Deserialize)]
94struct CargoTarget {
95    kind: Vec<String>,
96    src_path: PathBuf,
97}
98
99fn canonical_directory(path: &Path) -> Result<PathBuf, RustProjectError> {
100    fs::canonicalize(path).map_err(|error| RustProjectError::Io {
101        path: path.to_owned(),
102        reason: error.to_string(),
103    })
104}
105
106fn confined_relative(root: &Path, path: &Path) -> Result<String, RustProjectError> {
107    let relative = path
108        .strip_prefix(root)
109        .map_err(|_| RustProjectError::UnsafePath(path.display().to_string()))?;
110    if relative.as_os_str().is_empty()
111        || relative
112            .components()
113            .any(|component| !matches!(component, Component::Normal(_)))
114    {
115        return Err(RustProjectError::UnsafePath(path.display().to_string()));
116    }
117    Ok(relative.to_string_lossy().replace('\\', "/"))
118}
119
120fn cargo_metadata(root: &Path) -> Result<CargoMetadata, RustProjectError> {
121    let target_directory = root.join(".supercov/rust-target");
122    let output = Command::new("cargo")
123        .args(["metadata", "--format-version=1", "--no-deps"])
124        .current_dir(root)
125        .env("CARGO_TARGET_DIR", &target_directory)
126        .output()
127        .map_err(|error| RustProjectError::MetadataLaunch(error.to_string()))?;
128    if !output.status.success() {
129        return Err(RustProjectError::MetadataFailed(
130            String::from_utf8_lossy(&output.stderr).trim().to_owned(),
131        ));
132    }
133    serde_json::from_slice(&output.stdout)
134        .map_err(|error| RustProjectError::MetadataJson(error.to_string()))
135}
136
137/// The files rustc compiles for the given crate roots: each root and,
138/// transitively, every module it declares with `mod name;` (resolved the way
139/// rustc resolves it, `#[path]` included) and every file it pulls in with a
140/// literal `include!("....rs")`. A `.rs` file under the package that no module
141/// reaches -- a runtime source embedded as data with `include_str!`, a test
142/// fixture, a snippet -- is not part of any crate, so instrumenting it would
143/// change the data and count code that is never compiled.
144///
145/// A file that does not exist is skipped, not an error: a `#[cfg]`-gated
146/// module may name a file the checkout lacks, and rustc only complains when
147/// that cfg is active. Files outside the workspace are left alone as well.
148fn resolve_module_tree(
149    workspace: &Path,
150    roots: &BTreeSet<PathBuf>,
151    files: &mut BTreeSet<PathBuf>,
152) -> Result<(), RustProjectError> {
153    let canonical_workspace = canonical_directory(workspace)?;
154    // (file, directory its `mod` children resolve in)
155    let mut pending = roots
156        .iter()
157        .map(|root| (root.clone(), owner_directory(root)))
158        .collect::<Vec<_>>();
159    while let Some((file, directory)) = pending.pop() {
160        // `#[path = "../src/shared.rs"]` climbs out of its directory; the
161        // path is normalised lexically so the workspace check and the file
162        // set see one spelling of it.
163        let file = normalize(&file);
164        let directory = normalize(&directory);
165        if !file.starts_with(workspace) {
166            continue;
167        }
168        let Ok(metadata) = fs::symlink_metadata(&file) else {
169            continue;
170        };
171        // A symlink is followed only within the workspace: crossbeam shares
172        // one source file between its crates that way. The file is recorded
173        // under its target's path, so it is instrumented and digested once as
174        // a regular file; a symlink leaving the workspace would be
175        // instrumented in place, outside the copy, and is refused.
176        let file = if metadata.file_type().is_symlink() {
177            let target = fs::canonicalize(&file).map_err(|error| RustProjectError::Io {
178                path: file.clone(),
179                reason: error.to_string(),
180            })?;
181            if !target.starts_with(&canonical_workspace) || !target.is_file() {
182                return Err(RustProjectError::UnsafePath(file.display().to_string()));
183            }
184            target
185        } else if metadata.is_file() {
186            file.clone()
187        } else {
188            continue;
189        };
190        if !files.insert(file.clone()) {
191            continue;
192        }
193        let source = fs::read_to_string(&file).map_err(|error| RustProjectError::Io {
194            path: file.clone(),
195            reason: error.to_string(),
196        })?;
197        let parsed = SourceFile::parse(&source, Edition::CURRENT).tree();
198        collect_module_declarations(parsed.items(), &file, &directory, false, &mut pending);
199    }
200    Ok(())
201}
202
203/// Resolve `.` and `..` components without touching the filesystem.
204fn normalize(path: &Path) -> PathBuf {
205    let mut normalized = PathBuf::new();
206    for component in path.components() {
207        match component {
208            Component::ParentDir => {
209                normalized.pop();
210            }
211            Component::CurDir => {}
212            other => normalized.push(other.as_os_str()),
213        }
214    }
215    normalized
216}
217
218fn owner_directory(file: &Path) -> PathBuf {
219    file.parent().map_or_else(PathBuf::new, Path::to_path_buf)
220}
221
222/// Walk the items of one module body. `directory` is where this module's
223/// `mod name;` children live; `inline` says whether we are inside a
224/// `mod name { ... }` block, which changes what `#[path]` is relative to.
225fn collect_module_declarations(
226    items: impl Iterator<Item = ast::Item>,
227    file: &Path,
228    directory: &Path,
229    inline: bool,
230    pending: &mut Vec<(PathBuf, PathBuf)>,
231) {
232    for item in items {
233        match item {
234            ast::Item::Module(module) => {
235                let Some(name) = module.name() else {
236                    continue;
237                };
238                let name = name.text().to_string();
239                let path_attribute = module.attrs().find_map(|attr| {
240                    let is_path = attr
241                        .path()
242                        .is_some_and(|path| path.syntax().text() == "path");
243                    is_path.then(|| string_literal(attr.syntax())).flatten()
244                });
245                if let Some(list) = module.item_list() {
246                    let nested = directory.join(&name);
247                    collect_module_declarations(list.items(), file, &nested, true, pending);
248                } else if let Some(path) = path_attribute {
249                    // Relative to the file's own directory at the top level,
250                    // to the inline module's directory inside a block; the
251                    // loaded file owns its directory like a `mod.rs` does.
252                    let base = if inline {
253                        directory.to_path_buf()
254                    } else {
255                        owner_directory(file)
256                    };
257                    let target = base.join(path);
258                    let owner = owner_directory(&target);
259                    pending.push((target, owner));
260                } else {
261                    // `name.rs` and `name/mod.rs` both put their children in
262                    // `directory/name/`.
263                    let children = directory.join(&name);
264                    pending.push((directory.join(format!("{name}.rs")), children.clone()));
265                    pending.push((children.join("mod.rs"), children));
266                }
267            }
268            ast::Item::MacroCall(call) => {
269                let is_include = call.path().is_some_and(|path| {
270                    matches!(
271                        path.syntax().text().to_string().as_str(),
272                        "include" | "std::include" | "core::include" | "::std::include"
273                    )
274                });
275                if !is_include {
276                    continue;
277                }
278                let Some(literal) = string_literal(call.syntax()) else {
279                    continue;
280                };
281                if !literal.ends_with(".rs") {
282                    continue;
283                }
284                // Included code is spliced into this module: its own `mod`
285                // declarations resolve where this module's do.
286                pending.push((owner_directory(file).join(literal), directory.to_path_buf()));
287            }
288            _ => {}
289        }
290    }
291}
292
293/// The first string literal under a node, unescaped. Inside a macro's token
294/// tree the literal is a bare token, not a `Literal` node, so look at tokens.
295fn string_literal(node: &ra_ap_syntax::SyntaxNode) -> Option<String> {
296    node.descendants_with_tokens().find_map(|element| {
297        let string = ast::String::cast(element.into_token()?)?;
298        string.value().ok().map(|value| value.into_owned())
299    })
300}
301
302/// The crate roots of every workspace member: the source file of each Cargo
303/// target except build scripts, which Cargo compiles and runs on their own.
304fn crate_roots(
305    workspace: &Path,
306    packages: &[CargoPackage],
307) -> Result<BTreeSet<PathBuf>, RustProjectError> {
308    let mut roots = BTreeSet::new();
309    for package in packages {
310        let directory = package.manifest_path.parent().ok_or_else(|| {
311            RustProjectError::UnsafePath(package.manifest_path.display().to_string())
312        })?;
313        let directory = canonical_directory(directory)?;
314        confined_relative(workspace, &directory).or_else(|error| {
315            (directory == workspace)
316                .then_some(String::new())
317                .ok_or(error)
318        })?;
319        for target in &package.targets {
320            if target.kind.iter().any(|kind| kind == "custom-build") {
321                continue;
322            }
323            let root =
324                fs::canonicalize(&target.src_path).map_err(|error| RustProjectError::Io {
325                    path: target.src_path.clone(),
326                    reason: error.to_string(),
327                })?;
328            confined_relative(workspace, &root)?;
329            roots.insert(root);
330        }
331    }
332    Ok(roots)
333}
334
335/// Read-only Cargo workspace source discovery used by integrity checks. This
336/// deliberately shares the same path policy as transformation preparation.
337pub fn discover_rust_source_files(workspace: &Path) -> Result<Vec<String>, RustProjectError> {
338    let workspace = canonical_directory(workspace)?;
339    let metadata = cargo_metadata(&workspace)?;
340    let metadata_root = canonical_directory(&metadata.workspace_root)?;
341    if metadata_root != workspace {
342        return Err(RustProjectError::UnsafePath(
343            metadata.workspace_root.display().to_string(),
344        ));
345    }
346    let members = metadata
347        .workspace_members
348        .into_iter()
349        .collect::<BTreeSet<_>>();
350    let packages = metadata
351        .packages
352        .into_iter()
353        .filter(|package| members.contains(&package.id))
354        .collect::<Vec<_>>();
355    if packages.is_empty() {
356        return Err(RustProjectError::NoWorkspacePackages);
357    }
358    let mut files = BTreeSet::new();
359    resolve_module_tree(&workspace, &crate_roots(&workspace, &packages)?, &mut files)?;
360    if files.is_empty() {
361        return Err(RustProjectError::NoSourceFiles);
362    }
363    files
364        .into_iter()
365        .map(|path| confined_relative(&workspace, &path))
366        .collect()
367}
368
369fn runtime_module_name(sources: &BTreeMap<String, String>) -> String {
370    let mut suffix = 0_usize;
371    loop {
372        let candidate = if suffix == 0 {
373            "__supercov_runtime_v1".to_owned()
374        } else {
375            format!("__supercov_runtime_v1_{suffix}")
376        };
377        if sources.values().all(|source| !source.contains(&candidate)) {
378            return candidate;
379        }
380        suffix += 1;
381    }
382}
383
384/// Twelve hex digits identifying an instrumentation: a digest of every
385/// obligation ID in the manifest. Two builds of the same sources share it;
386/// any other program's instrumentation, such as a fixture a test prepares
387/// and runs, has another.
388pub fn manifest_token(manifest: &CoverageManifest) -> String {
389    let mut ids = manifest
390        .points
391        .iter()
392        .map(|point| point.id.as_str())
393        .chain(
394            manifest
395                .decisions
396                .iter()
397                .map(|decision| decision.id.as_str()),
398        )
399        .chain(manifest.branches.iter().flat_map(|branch| {
400            branch
401                .alternatives
402                .iter()
403                .map(|alternative| alternative.id.as_str())
404        }))
405        .collect::<Vec<_>>();
406    ids.sort_unstable();
407    ids.dedup();
408    let mut hasher = Sha256::new();
409    for id in ids {
410        hasher.update(id.as_bytes());
411        hasher.update(b"\n");
412    }
413    hex(&hasher.finalize()[..6])
414}
415
416/// The runtime names its evidence files `<crate key>-<pid>.events`; the key
417/// is the manifest token followed by a digest of the crate root, so the
418/// reader can tell this instrumentation's files from any other's and two
419/// crates of one process write separate files.
420fn crate_key(token: &str, path: &str) -> String {
421    format!("{token}{}", hex(&Sha256::digest(path.as_bytes())[..6]))
422}
423
424fn hex(bytes: &[u8]) -> String {
425    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
426}
427
428fn merge_manifest(
429    destination: &mut CoverageManifest,
430    mut source: CoverageManifest,
431) -> Result<(), RustProjectError> {
432    let mut ids = destination
433        .points
434        .iter()
435        .map(|point| point.id.as_str())
436        .chain(
437            destination
438                .decisions
439                .iter()
440                .map(|decision| decision.id.as_str()),
441        )
442        .chain(destination.branches.iter().map(|branch| branch.id.as_str()))
443        .collect::<BTreeSet<_>>();
444    for id in source
445        .points
446        .iter()
447        .map(|point| point.id.as_str())
448        .chain(source.decisions.iter().map(|decision| decision.id.as_str()))
449        .chain(source.branches.iter().map(|branch| branch.id.as_str()))
450    {
451        if !ids.insert(id) {
452            return Err(RustProjectError::DuplicateObligation(id.into()));
453        }
454    }
455    destination.points.append(&mut source.points);
456    destination.decisions.append(&mut source.decisions);
457    destination.branches.append(&mut source.branches);
458    for limitation in source.limitations {
459        let id = limitation.get("id").and_then(|value| value.as_str());
460        if !destination
461            .limitations
462            .iter()
463            .any(|existing| existing.get("id").and_then(|value| value.as_str()) == id)
464        {
465            destination.limitations.push(limitation);
466        }
467    }
468    Ok(())
469}
470
471pub fn prepare_rust_project(workspace: &Path) -> Result<PreparedRustProject, RustProjectError> {
472    let workspace = canonical_directory(workspace)?;
473    let metadata = cargo_metadata(&workspace)?;
474    let metadata_root = canonical_directory(&metadata.workspace_root)?;
475    if metadata_root != workspace {
476        return Err(RustProjectError::UnsafePath(
477            metadata.workspace_root.display().to_string(),
478        ));
479    }
480    let members = metadata
481        .workspace_members
482        .into_iter()
483        .collect::<BTreeSet<_>>();
484    let packages = metadata
485        .packages
486        .into_iter()
487        .filter(|package| members.contains(&package.id))
488        .collect::<Vec<_>>();
489    if packages.is_empty() {
490        return Err(RustProjectError::NoWorkspacePackages);
491    }
492
493    let roots = crate_roots(&workspace, &packages)?;
494    let mut files = BTreeSet::new();
495    resolve_module_tree(&workspace, &roots, &mut files)?;
496    if files.is_empty() {
497        return Err(RustProjectError::NoSourceFiles);
498    }
499
500    let mut sources = BTreeMap::new();
501    for path in files {
502        let relative = confined_relative(&workspace, &path)?;
503        let source = fs::read_to_string(&path).map_err(|error| RustProjectError::Io {
504            path: path.clone(),
505            reason: error.to_string(),
506        })?;
507        sources.insert(relative, source);
508    }
509    let runtime_module = runtime_module_name(&sources);
510    let runtime_path = format!("crate::{runtime_module}");
511    let mut manifest = CoverageManifest {
512        unmeasured: Vec::new(),
513        decisions: Vec::new(),
514        points: Vec::new(),
515        branches: Vec::new(),
516        limitations: Vec::new(),
517        scope: None,
518    };
519    for (relative, source) in &sources {
520        let transformed =
521            instrument_rust_source(relative, source, &runtime_path).map_err(|error| {
522                RustProjectError::Instrument {
523                    file: relative.clone(),
524                    reason: error.to_string(),
525                }
526            })?;
527        merge_manifest(&mut manifest, transformed.manifest)?;
528        fs::write(workspace.join(relative), transformed.code).map_err(|error| {
529            RustProjectError::Io {
530                path: workspace.join(relative),
531                reason: error.to_string(),
532            }
533        })?;
534    }
535
536    let token = manifest_token(&manifest);
537    let mut crate_roots = Vec::new();
538    for root in roots {
539        let relative = confined_relative(&workspace, &root)?;
540        let runtime = render_rust_runtime(&runtime_module, &crate_key(&token, &relative))
541            .map_err(RustProjectError::Runtime)?;
542        let mut source = fs::read_to_string(&root).map_err(|error| RustProjectError::Io {
543            path: root.clone(),
544            reason: error.to_string(),
545        })?;
546        source.push('\n');
547        source.push_str(&runtime);
548        fs::write(&root, source).map_err(|error| RustProjectError::Io {
549            path: root,
550            reason: error.to_string(),
551        })?;
552        crate_roots.push(relative);
553    }
554
555    manifest
556        .points
557        .sort_by(|left, right| left.id.cmp(&right.id));
558    manifest
559        .decisions
560        .sort_by(|left, right| left.id.cmp(&right.id));
561    manifest
562        .branches
563        .sort_by(|left, right| left.id.cmp(&right.id));
564    manifest.limitations.sort_by(|left, right| {
565        left.get("id")
566            .and_then(|value| value.as_str())
567            .cmp(&right.get("id").and_then(|value| value.as_str()))
568    });
569    let target_directory = metadata.target_directory;
570    let target_directory = if target_directory.is_absolute() {
571        target_directory
572    } else {
573        workspace.join(target_directory)
574    };
575    if !target_directory.starts_with(&workspace) {
576        return Err(RustProjectError::UnsafePath(
577            target_directory.display().to_string(),
578        ));
579    }
580    Ok(PreparedRustProject {
581        workspace_root: workspace,
582        target_directory,
583        source_files: sources.into_keys().collect(),
584        crate_roots,
585        runtime_module,
586        manifest,
587    })
588}
589
590#[cfg(test)]
591mod tests {
592    use std::{
593        process::Command,
594        sync::atomic::{AtomicU64, Ordering},
595        time::{SystemTime, UNIX_EPOCH},
596    };
597
598    use super::*;
599
600    fn fixture() -> PathBuf {
601        // One test calls this today, so nothing can collide with it yet. The
602        // counter is here because the clock is not enough on its own: it ticks
603        // once per microsecond and every test shares the pid, so the second
604        // test to use this helper would draw the same root as the first when
605        // the two start together.
606        static UNIQUE: AtomicU64 = AtomicU64::new(0);
607        let nonce = SystemTime::now()
608            .duration_since(UNIX_EPOCH)
609            .unwrap()
610            .as_nanos();
611        let root = std::env::temp_dir().join(format!(
612            "supercov-rust-project-{}-{nonce}-{}",
613            std::process::id(),
614            UNIQUE.fetch_add(1, Ordering::Relaxed)
615        ));
616        fs::create_dir(&root).unwrap();
617        fs::create_dir(root.join("src")).unwrap();
618        fs::create_dir(root.join("tests")).unwrap();
619        fs::write(
620            root.join("Cargo.toml"),
621            "[package]\nname='rust-project-fixture'\nversion='0.0.0'\nedition='2024'\n",
622        )
623        .unwrap();
624        fs::write(
625            root.join("src/lib.rs"),
626            r#"pub fn choose(first: bool, second: bool) -> i32 {
627    if first && second { 7 } else { 3 }
628}
629
630#[cfg(test)]
631mod tests {
632    #[test]
633    fn unit_choice() {
634        assert_eq!(super::choose(true, true), 7);
635    }
636}
637"#,
638        )
639        .unwrap();
640        fs::write(
641            root.join("tests/integration.rs"),
642            r#"#[test]
643fn integration_choice() {
644    assert_eq!(rust_project_fixture::choose(false, true), 3);
645}
646"#,
647        )
648        .unwrap();
649        root
650    }
651
652    #[test]
653    fn only_files_the_module_tree_reaches_are_instrumented() {
654        let root = fixture();
655        fs::create_dir_all(root.join("src/nested")).unwrap();
656        fs::create_dir_all(root.join("src/deep/inner")).unwrap();
657        fs::create_dir_all(root.join("runtime-assets")).unwrap();
658        fs::write(
659            root.join("src/lib.rs"),
660            concat!(
661                "mod util;\n",
662                "mod nested;\n",
663                "#[path = \"renamed_file.rs\"]\n",
664                "mod renamed;\n",
665                "mod deep;\n",
666                "include!(\"included.rs\");\n",
667                "pub const EMBEDDED: &str = include_str!(\"../runtime-assets/embedded.rs\");\n",
668                "pub fn choose(first: bool, second: bool) -> i32 {\n",
669                "    if first && second { util::seven() } else { nested::three() }\n",
670                "}\n",
671            ),
672        )
673        .unwrap();
674        fs::write(root.join("src/util.rs"), "pub fn seven() -> i32 { 7 }\n").unwrap();
675        fs::write(
676            root.join("src/nested/mod.rs"),
677            "mod leaf;\npub fn three() -> i32 { leaf::three() }\n",
678        )
679        .unwrap();
680        fs::write(
681            root.join("src/nested/leaf.rs"),
682            "pub fn three() -> i32 { 3 }\n",
683        )
684        .unwrap();
685        fs::write(
686            root.join("src/renamed_file.rs"),
687            "pub fn renamed() -> i32 { 1 }\n",
688        )
689        .unwrap();
690        fs::write(
691            root.join("src/deep.rs"),
692            "pub mod inner {\n    mod block_child;\n    pub fn deep() -> i32 { block_child::v() }\n}\n",
693        )
694        .unwrap();
695        fs::write(
696            root.join("src/deep/inner/block_child.rs"),
697            "pub fn v() -> i32 { 9 }\n",
698        )
699        .unwrap();
700        fs::write(
701            root.join("src/included.rs"),
702            "pub fn included() -> i32 { 2 }\n",
703        )
704        .unwrap();
705        // serde_json's tests reach into src with `#[path = "../src/..."]`.
706        fs::write(
707            root.join("tests/integration.rs"),
708            concat!(
709                "#[path = \"../src/util.rs\"]\n",
710                "mod util;\n",
711                "#[test]\n",
712                "fn integration_choice() {\n",
713                "    assert_eq!(rust_project_fixture::choose(false, true), 3);\n",
714                "    assert_eq!(util::seven(), 7);\n",
715                "}\n",
716            ),
717        )
718        .unwrap();
719        // Data, not code: embedded verbatim and compiled by a consumer of
720        // its own, which would not know any runtime module of ours.
721        let embedded = "pub fn standalone() -> i32 { if true { 1 } else { 0 } }\n";
722        fs::write(root.join("runtime-assets/embedded.rs"), embedded).unwrap();
723        fs::write(
724            root.join("src/orphan.rs"),
725            "pub fn unreachable_module() {}\n",
726        )
727        .unwrap();
728
729        let prepared = prepare_rust_project(&root).unwrap();
730        assert_eq!(
731            prepared.source_files,
732            [
733                "src/deep.rs",
734                "src/deep/inner/block_child.rs",
735                "src/included.rs",
736                "src/lib.rs",
737                "src/nested/leaf.rs",
738                "src/nested/mod.rs",
739                "src/renamed_file.rs",
740                "src/util.rs",
741                "tests/integration.rs",
742            ]
743        );
744        assert_eq!(
745            fs::read_to_string(root.join("runtime-assets/embedded.rs")).unwrap(),
746            embedded
747        );
748        assert!(
749            !fs::read_to_string(root.join("src/orphan.rs"))
750                .unwrap()
751                .contains("__supercov")
752        );
753        assert!(
754            fs::read_to_string(root.join("src/deep/inner/block_child.rs"))
755                .unwrap()
756                .contains("__supercov")
757        );
758        let build = Command::new("cargo")
759            .args(["test", "--no-run"])
760            .current_dir(&root)
761            .env("CARGO_TARGET_DIR", &prepared.target_directory)
762            .output()
763            .unwrap();
764        assert!(
765            build.status.success(),
766            "{}",
767            String::from_utf8_lossy(&build.stderr)
768        );
769        fs::remove_dir_all(root).unwrap();
770    }
771
772    #[cfg(unix)]
773    #[test]
774    fn a_module_shared_through_a_symlink_is_instrumented_once() {
775        let root = fixture();
776        fs::write(root.join("src/shared.rs"), "pub fn shared() -> i32 { 5 }\n").unwrap();
777        std::os::unix::fs::symlink("../src/shared.rs", root.join("tests/shared.rs")).unwrap();
778        fs::write(
779            root.join("src/lib.rs"),
780            concat!(
781                "pub mod shared;\n",
782                "pub fn choose(first: bool, second: bool) -> i32 {\n",
783                "    if first && second { 7 } else { shared::shared() }\n",
784                "}\n",
785            ),
786        )
787        .unwrap();
788        fs::write(
789            root.join("tests/integration.rs"),
790            concat!(
791                "mod shared;\n",
792                "#[test]\n",
793                "fn integration_choice() {\n",
794                "    assert_eq!(rust_project_fixture::choose(false, true), 5);\n",
795                "    assert_eq!(shared::shared(), 5);\n",
796                "}\n",
797            ),
798        )
799        .unwrap();
800        let prepared = prepare_rust_project(&root).unwrap();
801        // The target's path, once; never the symlink's spelling.
802        let shared = prepared
803            .source_files
804            .iter()
805            .filter(|file| file.ends_with("shared.rs"))
806            .collect::<Vec<_>>();
807        assert_eq!(shared, ["src/shared.rs"], "{:?}", prepared.source_files);
808        // The one function in it carries one function probe: instrumented
809        // once, through whichever spelling reached it first.
810        let instrumented = fs::read_to_string(root.join("src/shared.rs")).unwrap();
811        assert_eq!(instrumented.matches("rs:function:").count(), 1);
812        let build = Command::new("cargo")
813            .args(["test", "--no-run"])
814            .current_dir(&root)
815            .env("CARGO_TARGET_DIR", &prepared.target_directory)
816            .output()
817            .unwrap();
818        assert!(
819            build.status.success(),
820            "{}",
821            String::from_utf8_lossy(&build.stderr)
822        );
823        fs::remove_dir_all(root).unwrap();
824    }
825
826    #[test]
827    fn crate_keys_carry_the_manifest_token() {
828        let root = fixture();
829        let prepared = prepare_rust_project(&root).unwrap();
830        let token = manifest_token(&prepared.manifest);
831        assert_eq!(token.len(), 12);
832        assert!(token.bytes().all(|byte| byte.is_ascii_hexdigit()));
833        assert_eq!(token, manifest_token(&prepared.manifest));
834        let key = crate_key(&token, "src/lib.rs");
835        assert_eq!(key.len(), 24);
836        assert!(key.starts_with(&token));
837        assert_ne!(key, crate_key(&token, "tests/integration.rs"));
838        for crate_root in &prepared.crate_roots {
839            assert!(
840                fs::read_to_string(root.join(crate_root))
841                    .unwrap()
842                    .contains(&crate_key(&token, crate_root))
843            );
844        }
845        fs::remove_dir_all(root).unwrap();
846    }
847
848    #[test]
849    fn prepares_every_workspace_crate_root_and_compiles_without_manifest_changes() {
850        let root = fixture();
851        let manifest_before = fs::read(root.join("Cargo.toml")).unwrap();
852        let prepared = prepare_rust_project(&root).unwrap();
853        assert_eq!(
854            prepared.source_files,
855            ["src/lib.rs", "tests/integration.rs"]
856        );
857        assert_eq!(prepared.crate_roots, ["src/lib.rs", "tests/integration.rs"]);
858        assert!(!prepared.manifest.points.is_empty());
859        assert!(!prepared.manifest.decisions.is_empty());
860        assert_eq!(fs::read(root.join("Cargo.toml")).unwrap(), manifest_before);
861        for crate_root in &prepared.crate_roots {
862            assert!(
863                fs::read_to_string(root.join(crate_root))
864                    .unwrap()
865                    .contains(&format!("mod {}", prepared.runtime_module))
866            );
867        }
868        let build = Command::new("cargo")
869            .args(["test", "--no-run"])
870            .current_dir(&root)
871            .env("CARGO_TARGET_DIR", &prepared.target_directory)
872            .output()
873            .unwrap();
874        assert!(
875            build.status.success(),
876            "{}",
877            String::from_utf8_lossy(&build.stderr)
878        );
879        fs::remove_dir_all(root).unwrap();
880    }
881}