1use std::collections::BTreeSet;
18use std::ffi::{OsStr, OsString};
19use std::fs;
20use std::io;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23
24use crate::javascript_frontend::{authored_path, rewritten_files};
25
26pub const SOURCE_TOOL_COMMAND: &str = "__source-tool";
28
29pub const SOURCE_TOOLS_VARIABLE: &str = "SUPERCOV_SOURCE_TOOLS";
31
32pub const SOURCE_TOOLS: &[&str] = &["biome", "cspell", "dprint", "knip", "oxlint"];
36
37pub fn source_tools() -> BTreeSet<String> {
39 let mut tools = SOURCE_TOOLS
40 .iter()
41 .map(|tool| (*tool).to_owned())
42 .collect::<BTreeSet<_>>();
43 if let Some(named) = std::env::var_os(SOURCE_TOOLS_VARIABLE) {
44 tools.extend(
45 named
46 .to_string_lossy()
47 .split(',')
48 .map(str::trim)
49 .filter(|name| !name.is_empty() && !name.contains(['/', '\\']))
50 .map(str::to_owned),
51 );
52 }
53 tools
54}
55
56pub fn launcher(binary: &Path, tool: &Path) -> String {
58 let quoted = |path: &Path| format!("'{}'", path.to_string_lossy().replace('\'', "'\\''"));
59 format!(
60 "#!/bin/sh\n# Runs {name} on the project's source as it was written; see `supercov --help`.\nexec {binary} {SOURCE_TOOL_COMMAND} {tool} \"$@\"\n",
61 name = tool
62 .file_name()
63 .unwrap_or_default()
64 .to_string_lossy()
65 .replace('\n', " "),
66 binary = quoted(binary),
67 tool = quoted(tool),
68 )
69}
70
71fn command_launcher(binary: &Path, tool: &Path) -> String {
73 format!(
74 "@ECHO off\r\n\"{}\" {SOURCE_TOOL_COMMAND} \"{}\" %*\r\n",
75 binary.display(),
76 tool.display()
77 )
78}
79
80fn powershell_launcher(binary: &Path, tool: &Path) -> String {
82 let quoted = |path: &Path| format!("'{}'", path.to_string_lossy().replace('\'', "''"));
83 format!(
84 "& {} {SOURCE_TOOL_COMMAND} {} @args\r\nexit $LASTEXITCODE\r\n",
85 quoted(binary),
86 quoted(tool)
87 )
88}
89
90pub fn launcher_for(
97 judges: &BTreeSet<String>,
98 binary: &Path,
99 tools: &Path,
100 name: &OsStr,
101) -> Option<String> {
102 launcher_on(cfg!(windows), judges, binary, tools, name)
103}
104
105fn launcher_on(
106 windows: bool,
107 judges: &BTreeSet<String>,
108 binary: &Path,
109 tools: &Path,
110 name: &OsStr,
111) -> Option<String> {
112 let name = name.to_str()?;
113 if windows {
114 let (tool, kind) = match name.rsplit_once('.') {
115 Some((tool, extension)) if extension.eq_ignore_ascii_case("cmd") => (tool, 1),
116 Some((tool, extension)) if extension.eq_ignore_ascii_case("ps1") => (tool, 2),
117 _ => (name, 0),
118 };
119 if !judges.contains(tool) {
120 return None;
121 }
122 let shim = tools.join(format!("{tool}.cmd"));
123 return shim.is_file().then(|| match kind {
124 1 => command_launcher(binary, &shim),
125 2 => powershell_launcher(binary, &shim),
126 _ => launcher(binary, &shim),
127 });
128 }
129 judges
130 .contains(name)
131 .then(|| launcher(binary, &tools.join(name)))
132}
133
134pub fn launching_binary() -> Option<PathBuf> {
136 std::env::current_exe()
137 .ok()
138 .map(crate::workspace::simplified)
139}
140
141pub fn write_launcher(path: &Path, text: &str) -> io::Result<()> {
145 remove(path)?;
146 fs::write(path, text)?;
147 #[cfg(unix)]
148 {
149 use std::os::unix::fs::PermissionsExt;
150 fs::set_permissions(path, fs::Permissions::from_mode(0o755))?;
151 }
152 Ok(())
153}
154
155pub fn place_launchers(tools: &Path, to: &Path) -> io::Result<()> {
160 let judges = source_tools();
161 let (Some(binary), Ok(entries)) = (launching_binary(), fs::read_dir(tools)) else {
162 return Ok(());
163 };
164 if !fs::symlink_metadata(to).is_ok_and(|metadata| metadata.file_type().is_dir()) {
165 return Ok(());
166 }
167 for entry in entries {
168 let name = entry?.file_name();
169 if let Some(text) = launcher_for(&judges, &binary, tools, &name) {
170 write_launcher(&to.join(&name), &text)?;
171 }
172 }
173 Ok(())
174}
175
176pub fn path_with_launchers(workspace: &Path) -> Option<OsString> {
183 let path = std::env::var_os("PATH")?;
184 let binary = launching_binary()?;
185 let directory = workspace.join(".supercov/node_modules/.tools");
186 remove(&directory).ok()?;
187 let forms: &[&str] = if cfg!(windows) {
190 &[".exe", ".cmd", ".bat"]
191 } else {
192 &[""]
193 };
194 let mut found = false;
195 for name in source_tools() {
196 let Some(tool) = std::env::split_paths(&path)
197 .flat_map(|directory| {
198 forms
199 .iter()
200 .map(|form| directory.join(format!("{name}{form}")))
201 .collect::<Vec<_>>()
202 })
203 .find(|candidate| executable(candidate))
204 else {
205 continue;
206 };
207 fs::create_dir_all(&directory).ok()?;
208 if cfg!(windows) {
209 let launcher = command_launcher(&binary, &tool);
210 write_launcher(&directory.join(format!("{name}.cmd")), &launcher).ok()?;
211 } else {
212 write_launcher(&directory.join(&name), &launcher(&binary, &tool)).ok()?;
213 }
214 found = true;
215 }
216 if !found {
217 return None;
218 }
219 std::env::join_paths(std::iter::once(directory).chain(std::env::split_paths(&path))).ok()
220}
221
222#[cfg(unix)]
223fn executable(path: &Path) -> bool {
224 use std::os::unix::fs::PermissionsExt;
225 fs::metadata(path)
226 .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
227}
228
229#[cfg(not(unix))]
230fn executable(path: &Path) -> bool {
231 path.is_file()
232}
233
234pub fn source_view_path(workspace: &Path) -> Option<PathBuf> {
237 let name = workspace.file_name()?;
238 let kind = workspace.parent()?;
239 let container = kind.parent()?;
240 (kind.file_name()? == "workspace").then(|| container.join("source").join(name))
241}
242
243const BOUNDARY: &str = ".git";
250
251#[derive(Default)]
253struct View {
254 linked: BTreeSet<PathBuf>,
256 authored: BTreeSet<PathBuf>,
258}
259
260#[cfg(unix)]
261fn same_file(left: &fs::Metadata, right: &fs::Metadata) -> bool {
262 use std::os::unix::fs::MetadataExt;
263 left.dev() == right.dev() && left.ino() == right.ino()
264}
265
266#[cfg(not(unix))]
269fn same_file(left: &fs::Metadata, right: &fs::Metadata) -> bool {
270 left.len() == right.len()
271 && left.modified().is_ok()
272 && left.modified().ok() == right.modified().ok()
273}
274
275fn link_to(target: &Path, link: &Path) -> io::Result<()> {
278 crate::workspace::create_link(target, link, target.is_dir())
279}
280
281fn remove(path: &Path) -> io::Result<()> {
282 match fs::symlink_metadata(path) {
283 Ok(metadata) if metadata.file_type().is_dir() => fs::remove_dir_all(path),
284 Ok(_) => fs::remove_file(path).or_else(|_| fs::remove_dir(path)),
286 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
287 Err(error) => Err(error),
288 }
289}
290
291fn point(link: &Path, target: &Path) -> io::Result<()> {
293 if fs::read_link(link).is_ok_and(|current| current == target) {
294 return Ok(());
295 }
296 remove(link)?;
297 link_to(target, link)
298}
299
300fn link_file(from: &Path, to: &Path) -> io::Result<()> {
302 let source = fs::metadata(from)?;
303 if fs::symlink_metadata(to).is_ok_and(|current| same_file(¤t, &source)) {
304 return Ok(());
305 }
306 remove(to)?;
307 fs::hard_link(from, to).or_else(|_| fs::copy(from, to).map(|_| ()))
308}
309
310fn copy_file(from: &Path, to: &Path) -> io::Result<()> {
312 let contents = fs::read(from)?;
313 if fs::symlink_metadata(to).is_ok_and(|current| current.is_file())
314 && fs::read(to).is_ok_and(|current| current == contents)
315 {
316 return Ok(());
317 }
318 remove(to)?;
319 fs::write(to, contents)
320}
321
322fn refresh(workspace: &Path, view: &Path, dependencies: &Path) -> io::Result<View> {
327 let rewritten = rewritten_files(workspace)
328 .into_iter()
329 .map(PathBuf::from)
330 .collect::<BTreeSet<_>>();
331 let mut state = View::default();
332 fs::create_dir_all(view)?;
333 refresh_directory(
334 workspace,
335 view,
336 dependencies,
337 Path::new(""),
338 &rewritten,
339 &mut state,
340 )?;
341 Ok(state)
342}
343
344fn refresh_directory(
345 workspace: &Path,
346 view: &Path,
347 dependencies: &Path,
348 local: &Path,
349 rewritten: &BTreeSet<PathBuf>,
350 state: &mut View,
351) -> io::Result<()> {
352 let mut present = BTreeSet::new();
353 for entry in fs::read_dir(workspace.join(local))? {
354 let entry = entry?;
355 let name = entry.file_name();
356 if name == ".supercov" {
357 continue;
358 }
359 let path = local.join(&name);
360 let (from, to) = (workspace.join(&path), view.join(&path));
361 let file_type = entry.file_type()?;
362 present.insert(name.clone());
363 if name == "node_modules" {
364 let target = if local.as_os_str().is_empty() && dependencies.is_dir() {
365 dependencies.to_path_buf()
366 } else {
367 from
368 };
369 point(&to, &target)?;
370 } else if file_type.is_symlink() {
371 point(&to, &fs::read_link(&from)?)?;
372 } else if file_type.is_dir() {
373 if !fs::symlink_metadata(&to).is_ok_and(|current| current.file_type().is_dir()) {
374 remove(&to)?;
375 fs::create_dir(&to)?;
376 }
377 refresh_directory(workspace, view, dependencies, &path, rewritten, state)?;
378 } else if file_type.is_file() {
379 let authored = authored_path(workspace, &path);
380 if rewritten.contains(&path) && authored.is_file() {
381 copy_file(&authored, &to)?;
382 state.authored.insert(path);
383 } else {
384 link_file(&from, &to)?;
385 state.linked.insert(path);
386 }
387 }
388 }
389 let boundary = local.as_os_str().is_empty() && !present.contains(OsStr::new(BOUNDARY));
390 if boundary && !view.join(BOUNDARY).is_dir() {
391 remove(&view.join(BOUNDARY))?;
392 fs::create_dir(view.join(BOUNDARY))?;
393 }
394 for entry in fs::read_dir(view.join(local))? {
395 let entry = entry?;
396 let kept =
397 present.contains(&entry.file_name()) || (boundary && entry.file_name() == BOUNDARY);
398 if !kept {
399 remove(&entry.path())?;
400 }
401 }
402 Ok(())
403}
404
405#[derive(Default)]
407struct Outcome {
408 changed_sources: Vec<PathBuf>,
410}
411
412fn reconcile(workspace: &Path, view: &Path, state: &View) -> io::Result<Outcome> {
420 let mut outcome = Outcome::default();
421 for path in &state.linked {
422 if fs::symlink_metadata(view.join(path)).is_err() {
423 remove(&workspace.join(path))?;
424 }
425 }
426 for path in &state.authored {
427 let unchanged = fs::read(view.join(path))
428 .and_then(|current| Ok(current == fs::read(authored_path(workspace, path))?))
429 .unwrap_or(false);
430 if !unchanged {
431 outcome.changed_sources.push(path.clone());
432 }
433 }
434 reconcile_directory(workspace, view, Path::new(""), state)?;
435 Ok(outcome)
436}
437
438fn reconcile_directory(
439 workspace: &Path,
440 view: &Path,
441 local: &Path,
442 state: &View,
443) -> io::Result<()> {
444 for entry in fs::read_dir(view.join(local))? {
445 let entry = entry?;
446 let path = local.join(entry.file_name());
447 let (from, to) = (view.join(&path), workspace.join(&path));
448 let file_type = entry.file_type()?;
449 if path == Path::new(BOUNDARY) && fs::symlink_metadata(&to).is_err() {
450 continue;
451 }
452 if file_type.is_dir() {
453 if fs::symlink_metadata(&to).is_err() {
454 fs::create_dir(&to)?;
455 }
456 reconcile_directory(workspace, view, &path, state)?;
457 } else if file_type.is_file() && !state.authored.contains(&path) {
458 link_file(&from, &to)?;
459 } else if file_type.is_symlink()
460 && fs::symlink_metadata(&to).is_err()
461 && let Ok(target) = fs::read_link(&from)
462 {
463 link_to(&target, &to)?;
464 }
465 }
466 Ok(())
467}
468
469fn in_view(value: &OsStr, workspace: &[&Path], view: &Path) -> OsString {
471 let Some(text) = value.to_str() else {
472 return value.to_owned();
473 };
474 let mut translated = text.to_owned();
475 for workspace in workspace {
476 if let (Some(from), Some(to)) = (workspace.to_str(), view.to_str())
477 && !from.is_empty()
478 {
479 translated = translated.replace(from, to);
480 }
481 }
482 translated.into()
483}
484
485fn status(command: &mut Command) -> i32 {
486 match command.status() {
487 Ok(status) => status.code().unwrap_or(1),
488 Err(error) => {
489 eprintln!(
490 "[supercov] could not start {}: {error}",
491 command.get_program().to_string_lossy()
492 );
493 127
494 }
495 }
496}
497
498pub fn run_source_tool(tool: &Path, arguments: &[OsString]) -> i32 {
502 let run_as_is = || status(Command::new(tool).args(arguments));
503 let Some(named) = std::env::var_os("SUPERCOV_PROJECT_ROOT").map(PathBuf::from) else {
504 return run_as_is();
505 };
506 let (Ok(workspace), Ok(directory)) = (
509 crate::workspace::canonicalize_simplified(&named),
510 std::env::current_dir().and_then(crate::workspace::canonicalize_simplified),
511 ) else {
512 return run_as_is();
513 };
514 let (Ok(local), Some(view)) = (
515 directory.strip_prefix(&workspace),
516 source_view_path(&workspace),
517 ) else {
518 return run_as_is();
519 };
520 let dependencies = std::env::var_os("SUPERCOV_SOURCE_PROJECT_ROOT")
521 .map(|root| PathBuf::from(root).join("node_modules"))
522 .unwrap_or_else(|| workspace.join("node_modules"));
523 let state = match refresh(&workspace, &view, &dependencies) {
524 Ok(state) => state,
525 Err(error) => {
526 eprintln!(
527 "[supercov] {} reads the instrumented files: the project's source could not be laid out for it at {} ({error})",
528 tool.display(),
529 view.display()
530 );
531 return run_as_is();
532 }
533 };
534 let names = [workspace.as_path(), named.as_path()];
535 let mut command = Command::new(tool);
536 command.current_dir(view.join(local)).args(
537 arguments
538 .iter()
539 .map(|argument| in_view(argument, &names, &view)),
540 );
541 for (name, value) in std::env::vars_os() {
542 let name_text = name.to_string_lossy();
548 if ["PWD", "OLDPWD", "INIT_CWD", "PATH"]
549 .iter()
550 .any(|location| name_text.eq_ignore_ascii_case(location))
551 || name_text.starts_with("npm_")
552 {
553 let translated = in_view(&value, &names, &view);
554 if translated != value {
555 command.env(name, translated);
556 }
557 }
558 }
559 let code = status(&mut command);
560 match reconcile(&workspace, &view, &state) {
561 Ok(outcome) if !outcome.changed_sources.is_empty() => {
562 let listed = outcome
563 .changed_sources
564 .iter()
565 .take(3)
566 .map(|path| path.display().to_string())
567 .collect::<Vec<_>>()
568 .join(", ");
569 eprintln!(
570 "[supercov] {} changed {} source file(s) ({listed}{}); a measured run does not apply a tool's changes to source, so run it without Supercov to keep them",
571 tool.file_name().unwrap_or_default().to_string_lossy(),
572 outcome.changed_sources.len(),
573 if outcome.changed_sources.len() > 3 {
574 ", …"
575 } else {
576 ""
577 },
578 );
579 }
580 Ok(_) => {}
581 Err(error) => eprintln!(
582 "[supercov] what {} wrote could not be brought into the workspace: {error}",
583 tool.display()
584 ),
585 }
586 code
587}
588
589#[cfg(all(test, unix))]
590mod tests {
591 use super::*;
592 use std::sync::atomic::{AtomicU64, Ordering};
593
594 static UNIQUE: AtomicU64 = AtomicU64::new(0);
595
596 fn workspace() -> (PathBuf, PathBuf, PathBuf) {
599 let root = std::env::temp_dir().join(format!(
600 "supercov-source-view-{}-{}-{}",
601 std::process::id(),
602 std::time::SystemTime::now()
603 .duration_since(std::time::UNIX_EPOCH)
604 .unwrap()
605 .as_nanos(),
606 UNIQUE.fetch_add(1, Ordering::Relaxed)
607 ));
608 let workspace = root.join("workspaces/workspace/project");
609 fs::create_dir_all(workspace.join("src")).unwrap();
610 fs::create_dir_all(workspace.join("node_modules/.bin")).unwrap();
611 fs::create_dir_all(workspace.join(".supercov/node_modules/.authored/src")).unwrap();
612 fs::write(workspace.join("src/a.js"), "probe(); const a = 1;\n").unwrap();
613 fs::write(
614 workspace.join(".supercov/node_modules/.authored/src/a.js"),
615 "const a = 1;\n",
616 )
617 .unwrap();
618 fs::write(
619 workspace.join(".supercov/node_modules/authored-sources.json"),
620 "[\"src/a.js\"]",
621 )
622 .unwrap();
623 fs::write(workspace.join("README.md"), "read me\n").unwrap();
624 let dependencies = root.join("project/node_modules");
625 fs::create_dir_all(&dependencies).unwrap();
626 let view = source_view_path(&workspace).unwrap();
627 (workspace, view, dependencies)
628 }
629
630 #[test]
631 fn a_view_holds_each_file_as_its_author_wrote_it() {
632 let (workspace, view, dependencies) = workspace();
633 let state = refresh(&workspace, &view, &dependencies).unwrap();
634 assert_eq!(
635 fs::read_to_string(view.join("src/a.js")).unwrap(),
636 "const a = 1;\n"
637 );
638 assert!(same_file(
639 &fs::metadata(view.join("README.md")).unwrap(),
640 &fs::metadata(workspace.join("README.md")).unwrap()
641 ));
642 assert_eq!(
643 fs::read_link(view.join("node_modules")).unwrap(),
644 dependencies
645 );
646 assert!(!view.join(".supercov").exists());
647 assert_eq!(state.authored.len(), 1);
648 assert!(view.join(".git").is_dir());
649
650 fs::create_dir(view.join("reports")).unwrap();
653 fs::write(view.join("reports/lint.json"), "[]").unwrap();
654 fs::remove_file(view.join("README.md")).unwrap();
655 fs::write(view.join("README.md"), "replaced\n").unwrap();
656 let outcome = reconcile(&workspace, &view, &state).unwrap();
657 assert_eq!(
658 fs::read_to_string(workspace.join("reports/lint.json")).unwrap(),
659 "[]"
660 );
661 assert_eq!(
662 fs::read_to_string(workspace.join("README.md")).unwrap(),
663 "replaced\n"
664 );
665 assert!(outcome.changed_sources.is_empty());
666 assert!(!workspace.join(".git").exists());
667 assert_eq!(
669 fs::read_to_string(workspace.join("src/a.js")).unwrap(),
670 "probe(); const a = 1;\n"
671 );
672
673 fs::write(view.join("src/a.js"), "const a = 1\n").unwrap();
675 let outcome = reconcile(&workspace, &view, &state).unwrap();
676 assert_eq!(outcome.changed_sources, [PathBuf::from("src/a.js")]);
677 assert_eq!(
678 fs::read_to_string(workspace.join(".supercov/node_modules/.authored/src/a.js"))
679 .unwrap(),
680 "const a = 1;\n"
681 );
682
683 fs::remove_file(workspace.join("reports/lint.json")).unwrap();
685 let state = refresh(&workspace, &view, &dependencies).unwrap();
686 assert!(!view.join("reports/lint.json").exists());
687 assert_eq!(
688 fs::read_to_string(view.join("src/a.js")).unwrap(),
689 "const a = 1;\n"
690 );
691 fs::remove_file(view.join("README.md")).unwrap();
692 reconcile(&workspace, &view, &state).unwrap();
693 assert!(!workspace.join("README.md").exists());
694 fs::remove_dir_all(workspace.ancestors().nth(3).unwrap()).unwrap();
695 }
696
697 #[test]
698 fn a_launcher_survives_a_quote_in_a_path() {
699 let text = launcher(
700 Path::new("/opt/it's here/supercov"),
701 Path::new("/work/app/node_modules/.bin/biome"),
702 );
703 assert!(text.starts_with("#!/bin/sh\n"), "{text}");
704 assert!(
705 text.contains("exec '/opt/it'\\''s here/supercov' __source-tool '/work/app/node_modules/.bin/biome' \"$@\"\n"),
706 "{text}"
707 );
708 }
709
710 #[test]
711 fn a_windows_tool_gets_a_launcher_for_each_of_its_shims() {
712 let (workspace, ..) = workspace();
716 let tools = workspace.join("node_modules/.bin");
717 for file in ["biome", "biome.cmd", "biome.ps1", "jest.cmd"] {
718 fs::write(tools.join(file), "").unwrap();
719 }
720 let judges = BTreeSet::from(["biome".to_owned()]);
721 let binary = Path::new("C:/Program Files/supercov.exe");
722 let shim = tools.join("biome.cmd");
723 let launcher = |name: &str| launcher_on(true, &judges, binary, &tools, OsStr::new(name));
724 assert_eq!(
725 launcher("biome.cmd").unwrap(),
726 format!(
727 "@ECHO off\r\n\"C:/Program Files/supercov.exe\" __source-tool \"{}\" %*\r\n",
728 shim.display()
729 )
730 );
731 assert_eq!(
732 launcher("biome.ps1").unwrap(),
733 format!(
734 "& 'C:/Program Files/supercov.exe' __source-tool '{}' @args\r\nexit $LASTEXITCODE\r\n",
735 shim.display()
736 )
737 );
738 assert!(launcher("biome").unwrap().starts_with("#!/bin/sh\n"));
739 assert!(
740 launcher("biome")
741 .unwrap()
742 .contains(&format!("'{}'", shim.display()))
743 );
744 assert_eq!(launcher("jest.cmd"), None);
746 assert!(launcher_on(false, &judges, binary, &tools, OsStr::new("biome")).is_some());
748 assert_eq!(
749 launcher_on(false, &judges, binary, &tools, OsStr::new("biome.cmd")),
750 None
751 );
752 fs::remove_dir_all(workspace.ancestors().nth(3).unwrap()).unwrap();
753 }
754
755 #[test]
756 fn a_package_bin_of_its_own_gets_launchers_in_place() {
757 let (workspace, _, dependencies) = workspace();
762 let tools = dependencies.join(".bin");
763 fs::create_dir_all(&tools).unwrap();
764 fs::write(tools.join("oxlint"), "the project's own\n").unwrap();
765 fs::write(tools.join("jest"), "runs code\n").unwrap();
766 let copy = workspace.join("packages/app/node_modules/.bin");
767 fs::create_dir_all(©).unwrap();
768 fs::hard_link(tools.join("oxlint"), copy.join("oxlint")).unwrap();
769 fs::hard_link(tools.join("jest"), copy.join("jest")).unwrap();
770
771 place_launchers(&tools, ©).unwrap();
772
773 assert!(
774 fs::read_to_string(copy.join("oxlint"))
775 .unwrap()
776 .contains("__source-tool"),
777 );
778 assert_eq!(
779 fs::read_to_string(tools.join("oxlint")).unwrap(),
780 "the project's own\n"
781 );
782 assert_eq!(
783 fs::read_to_string(copy.join("jest")).unwrap(),
784 "runs code\n"
785 );
786 fs::remove_dir_all(workspace.ancestors().nth(3).unwrap()).unwrap();
787 }
788
789 #[test]
790 fn a_path_in_an_argument_is_the_views() {
791 let translated = in_view(
792 OsStr::new("--config=/w/workspaces/workspace/p/biome.json"),
793 &[Path::new("/w/workspaces/workspace/p")],
794 Path::new("/w/workspaces/source/p"),
795 );
796 assert_eq!(translated, "--config=/w/workspaces/source/p/biome.json");
797 }
798}