1use 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
137fn 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 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 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 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
203fn 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
222fn 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 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 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 pending.push((owner_directory(file).join(literal), directory.to_path_buf()));
287 }
288 _ => {}
289 }
290 }
291}
292
293fn 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
302fn 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
335pub 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
384pub 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
416fn 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 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 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 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 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 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}