1use crate::{
5 errors::{FromMessagesError, RustBuildMetaParseError, WriteTestListError},
6 helpers::convert_rel_path_to_forward_slash,
7 list::{BinaryListState, OutputFormat, RustBuildMeta, Styles},
8 platform::BuildPlatforms,
9 write_str::WriteStr,
10};
11use camino::{Utf8Path, Utf8PathBuf};
12use cargo_metadata::{Artifact, BuildScript, Message, PackageId, TargetKind};
13use guppy::graph::PackageGraph;
14use nextest_metadata::{
15 BinaryListSummary, BuildPlatform, RustBinaryId, RustNonTestBinaryKind,
16 RustNonTestBinarySummary, RustTestBinaryKind, RustTestBinarySummary,
17};
18use owo_colors::OwoColorize;
19use serde::Deserialize;
20use std::{
21 collections::{BTreeMap, HashSet},
22 io,
23};
24use tracing::{debug, warn};
25
26#[derive(Clone, Debug)]
28pub struct RustTestBinary {
29 pub id: RustBinaryId,
31 pub path: Utf8PathBuf,
33 pub package_id: String,
35 pub kind: RustTestBinaryKind,
37 pub name: String,
39 pub build_platform: BuildPlatform,
42}
43
44#[derive(Clone, Debug)]
46pub struct BinaryList {
47 pub rust_build_meta: RustBuildMeta<BinaryListState>,
49
50 pub rust_binaries: Vec<RustTestBinary>,
52}
53
54impl BinaryList {
55 pub fn from_messages(
57 reader: impl io::BufRead,
58 graph: &PackageGraph,
59 build_platforms: BuildPlatforms,
60 ) -> Result<Self, FromMessagesError> {
61 let mut builder = BinaryListBuilder::new(graph, build_platforms);
62
63 for message in Message::parse_stream(reader) {
64 let message = message.map_err(FromMessagesError::ReadMessages)?;
65 builder.process_message(message)?;
66 }
67
68 Ok(builder.finish())
69 }
70
71 pub fn from_summary(summary: BinaryListSummary) -> Result<Self, RustBuildMetaParseError> {
73 let rust_binaries = summary
74 .rust_binaries
75 .into_values()
76 .map(|bin| RustTestBinary {
77 name: bin.binary_name,
78 path: bin.binary_path,
79 package_id: bin.package_id,
80 kind: bin.kind,
81 id: bin.binary_id,
82 build_platform: bin.build_platform,
83 })
84 .collect();
85 Ok(Self {
86 rust_build_meta: RustBuildMeta::from_summary(summary.rust_build_meta)?,
87 rust_binaries,
88 })
89 }
90
91 pub fn write(
93 &self,
94 output_format: OutputFormat,
95 writer: &mut dyn WriteStr,
96 colorize: bool,
97 ) -> Result<(), WriteTestListError> {
98 match output_format {
99 OutputFormat::Human { verbose } => self
100 .write_human(writer, verbose, colorize)
101 .map_err(WriteTestListError::Io),
102 OutputFormat::Oneline { verbose } => self
103 .write_oneline(writer, verbose, colorize)
104 .map_err(WriteTestListError::Io),
105 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
106 }
107 }
108
109 fn to_summary(&self) -> BinaryListSummary {
110 BinaryListSummary {
111 rust_build_meta: self.rust_build_meta.to_summary(),
112 rust_binaries: self.binary_summaries(),
113 }
114 }
115
116 pub(crate) fn to_archive_summary(&self) -> BinaryListSummary {
123 let target_dir = &self.rust_build_meta.target_directory;
124 let build_directory = &self.rust_build_meta.build_directory;
125
126 let rust_binaries = self
127 .rust_binaries
128 .iter()
129 .map(|bin| {
130 let binary_path = target_dir.join(
134 bin.path
135 .strip_prefix(build_directory)
136 .expect("test binary paths must be within the build directory"),
137 );
138 let summary = RustTestBinarySummary {
139 binary_name: bin.name.clone(),
140 package_id: bin.package_id.clone(),
141 kind: bin.kind.clone(),
142 binary_path,
143 binary_id: bin.id.clone(),
144 build_platform: bin.build_platform,
145 };
146 (bin.id.clone(), summary)
147 })
148 .collect();
149
150 BinaryListSummary {
151 rust_build_meta: self.rust_build_meta.to_archive_summary(),
152 rust_binaries,
153 }
154 }
155
156 fn binary_summaries(&self) -> BTreeMap<RustBinaryId, RustTestBinarySummary> {
157 self.rust_binaries
158 .iter()
159 .map(|bin| {
160 let summary = RustTestBinarySummary {
161 binary_name: bin.name.clone(),
162 package_id: bin.package_id.clone(),
163 kind: bin.kind.clone(),
164 binary_path: bin.path.clone(),
165 binary_id: bin.id.clone(),
166 build_platform: bin.build_platform,
167 };
168 (bin.id.clone(), summary)
169 })
170 .collect()
171 }
172
173 fn write_human(
174 &self,
175 writer: &mut dyn WriteStr,
176 verbose: bool,
177 colorize: bool,
178 ) -> io::Result<()> {
179 let mut styles = Styles::default();
180 if colorize {
181 styles.colorize();
182 }
183 for bin in &self.rust_binaries {
184 if verbose {
185 writeln!(writer, "{}:", bin.id.style(styles.binary_id))?;
186 writeln!(writer, " {} {}", "bin:".style(styles.field), bin.path)?;
187 writeln!(
188 writer,
189 " {} {}",
190 "build platform:".style(styles.field),
191 bin.build_platform,
192 )?;
193 } else {
194 writeln!(writer, "{}", bin.id.style(styles.binary_id))?;
195 }
196 }
197 Ok(())
198 }
199
200 fn write_oneline(
201 &self,
202 writer: &mut dyn WriteStr,
203 verbose: bool,
204 colorize: bool,
205 ) -> io::Result<()> {
206 let mut styles = Styles::default();
207 if colorize {
208 styles.colorize();
209 }
210 for bin in &self.rust_binaries {
211 write!(writer, "{}", bin.id.style(styles.binary_id))?;
212 if verbose {
213 write!(
214 writer,
215 " [{}{}] [{}{}]",
216 "bin: ".style(styles.field),
217 bin.path,
218 "build platform: ".style(styles.field),
219 bin.build_platform,
220 )?;
221 }
222 writeln!(writer)?;
223 }
224 Ok(())
225 }
226
227 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
229 let mut s = String::with_capacity(1024);
230 self.write(output_format, &mut s, false)?;
231 Ok(s)
232 }
233}
234
235#[derive(Debug)]
237pub struct BinaryListBuilder<'g> {
238 state: BinaryListBuildState<'g>,
239}
240
241impl<'g> BinaryListBuilder<'g> {
242 pub fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
244 Self {
245 state: BinaryListBuildState::new(graph, build_platforms),
246 }
247 }
248
249 pub fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
251 self.state.process_message(message)
252 }
253
254 pub fn process_message_line(&mut self, line: &str) -> Result<(), FromMessagesError> {
259 self.process_message(parse_message_line(line))
260 }
261
262 pub fn finish(self) -> BinaryList {
264 self.state.finish()
265 }
266}
267
268fn parse_message_line(line: &str) -> Message {
270 let mut deserializer = serde_json::Deserializer::from_str(line);
271 deserializer.disable_recursion_limit();
272 Message::deserialize(&mut deserializer).unwrap_or_else(|_| Message::TextLine(line.to_owned()))
273}
274
275#[derive(Debug)]
276struct BinaryListBuildState<'g> {
277 graph: &'g PackageGraph,
278 rust_binaries: Vec<RustTestBinary>,
279 rust_build_meta: RustBuildMeta<BinaryListState>,
280 alt_target_dir: Option<Utf8PathBuf>,
281}
282
283impl<'g> BinaryListBuildState<'g> {
284 fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
285 let rust_target_dir = graph.workspace().target_directory().to_path_buf();
286 let build_directory = graph
289 .workspace()
290 .build_directory()
291 .unwrap_or_else(|| graph.workspace().target_directory())
292 .to_path_buf();
293 let alt_target_dir = std::env::var("__NEXTEST_ALT_TARGET_DIR")
295 .ok()
296 .map(Utf8PathBuf::from);
297
298 Self {
299 graph,
300 rust_binaries: vec![],
301 rust_build_meta: RustBuildMeta::new(rust_target_dir, build_directory, build_platforms),
302 alt_target_dir,
303 }
304 }
305
306 fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
307 match message {
308 Message::CompilerArtifact(artifact) => {
309 self.process_artifact(artifact)?;
310 }
311 Message::BuildScriptExecuted(build_script) => {
312 self.process_build_script(build_script)?;
313 }
314 _ => {
315 }
317 }
318
319 Ok(())
320 }
321
322 fn process_artifact(&mut self, artifact: Artifact) -> Result<(), FromMessagesError> {
323 if let Some(path) = artifact.executable {
324 self.detect_base_output_dir(&path);
325
326 if artifact.profile.test {
327 let package_id = artifact.package_id.repr;
328
329 let name = artifact.target.name;
332
333 let package = self
334 .graph
335 .metadata(&guppy::PackageId::new(package_id.clone()))
336 .map_err(FromMessagesError::PackageGraph)?;
337
338 let kind = artifact.target.kind;
339 if kind.is_empty() {
340 return Err(FromMessagesError::MissingTargetKind {
341 package_name: package.name().to_owned(),
342 binary_name: name.clone(),
343 });
344 }
345
346 let (computed_kind, platform) = if kind.iter().any(|k| {
347 matches!(
349 k,
350 TargetKind::Lib
351 | TargetKind::RLib
352 | TargetKind::DyLib
353 | TargetKind::CDyLib
354 | TargetKind::StaticLib
355 )
356 }) {
357 (RustTestBinaryKind::LIB, BuildPlatform::Target)
358 } else if let Some(TargetKind::ProcMacro) = kind.first() {
359 (RustTestBinaryKind::PROC_MACRO, BuildPlatform::Host)
360 } else {
361 (
363 RustTestBinaryKind::new(
364 kind.into_iter()
365 .next()
366 .expect("already checked that kind is non-empty")
367 .to_string(),
368 ),
369 BuildPlatform::Target,
370 )
371 };
372
373 let id = RustBinaryId::from_parts(package.name(), &computed_kind, &name);
375
376 self.rust_binaries.push(RustTestBinary {
377 path,
378 package_id,
379 kind: computed_kind,
380 name,
381 id,
382 build_platform: platform,
383 });
384 } else if artifact
385 .target
386 .kind
387 .iter()
388 .any(|x| matches!(x, TargetKind::Bin))
389 {
390 if let Ok(rel_path) = path.strip_prefix(&self.rust_build_meta.target_directory) {
394 let non_test_binary = RustNonTestBinarySummary {
395 name: artifact.target.name,
396 kind: RustNonTestBinaryKind::BIN_EXE,
397 path: convert_rel_path_to_forward_slash(rel_path),
398 build_platform: self.non_test_build_platform(&path),
399 };
400
401 self.rust_build_meta.non_test_binaries.insert(
402 guppy::PackageId::new(artifact.package_id.repr),
403 non_test_binary,
404 );
405 };
406 }
407 } else if artifact
408 .target
409 .kind
410 .iter()
411 .any(|x| matches!(x, TargetKind::DyLib | TargetKind::CDyLib))
412 {
413 for filename in artifact.filenames {
415 if let Ok(rel_path) = filename.strip_prefix(&self.rust_build_meta.target_directory)
416 {
417 let non_test_binary = RustNonTestBinarySummary {
418 name: artifact.target.name.clone(),
419 kind: RustNonTestBinaryKind::DYLIB,
420 path: convert_rel_path_to_forward_slash(rel_path),
421 build_platform: self.non_test_build_platform(&filename),
422 };
423 self.rust_build_meta.non_test_binaries.insert(
424 guppy::PackageId::new(artifact.package_id.repr.clone()),
425 non_test_binary,
426 );
427 }
428 }
429 }
430
431 Ok(())
432 }
433
434 fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
453 let rel_path = match artifact_path.strip_prefix(&self.rust_build_meta.build_directory) {
462 Ok(rel) => rel,
463 Err(_) => {
464 debug!(
465 target: "nextest-runner::list",
466 "artifact path `{}` is not within the build directory `{}`, \
467 skipping base output directory detection",
468 artifact_path, self.rust_build_meta.build_directory,
469 );
470 return None;
471 }
472 };
473
474 let base = base_output_dir(rel_path)?;
475 if !self.rust_build_meta.base_output_directories.contains(base) {
476 self.rust_build_meta
477 .base_output_directories
478 .insert(convert_rel_path_to_forward_slash(base));
479 }
480 Some(())
481 }
482
483 fn non_test_build_platform(&self, artifact_path: &Utf8Path) -> Option<BuildPlatform> {
484 artifact_build_platform(
485 artifact_path,
486 &self.rust_build_meta.target_directory,
487 &self.rust_build_meta.build_directory,
488 self.rust_build_meta
489 .build_platforms
490 .target
491 .as_ref()
492 .map(|target| target.triple.platform.triple_str()),
493 )
494 }
495
496 fn process_build_script(&mut self, build_script: BuildScript) -> Result<(), FromMessagesError> {
497 for path in build_script.linked_paths {
498 self.detect_linked_path(&build_script.package_id, &path);
499 }
500
501 let package_id = guppy::PackageId::new(build_script.package_id.repr);
503 let in_workspace = self.graph.metadata(&package_id).map_or_else(
504 |_| {
505 debug!(
506 target: "nextest-runner::list",
507 "package ID `{}` isn't in cargo metadata, so its build \
508 script's out_dir and env won't be recorded (this is \
509 expected for the standard library and its dependencies \
510 under -Zbuild-std)",
511 package_id
512 );
513 false
515 },
516 |p| p.in_workspace(),
517 );
518 if in_workspace {
519 match build_script
521 .out_dir
522 .strip_prefix(&self.rust_build_meta.build_directory)
523 {
524 Ok(rel_out_dir) => {
525 self.rust_build_meta.build_script_out_dirs.insert(
526 package_id.repr().to_owned(),
527 convert_rel_path_to_forward_slash(rel_out_dir),
528 );
529 }
530 Err(_) => {
531 debug!(
532 target: "nextest-runner::list",
533 "build script out_dir `{}` for package `{}` is not within \
534 the build directory `{}`, skipping",
535 build_script.out_dir, package_id,
536 self.rust_build_meta.build_directory,
537 );
538 }
539 }
540
541 if !build_script.env.is_empty() {
544 self.rust_build_meta
545 .build_script_info
546 .get_or_insert_with(BTreeMap::new)
547 .entry(package_id.repr().to_owned())
548 .or_default()
549 .envs = build_script.env.into_iter().collect();
550 }
551 }
552
553 Ok(())
554 }
555
556 fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
558 let actual_path = match path.as_str().split_once('=') {
560 Some((_, p)) => p.into(),
561 None => path,
562 };
563
564 let rel_path = match actual_path.strip_prefix(&self.rust_build_meta.build_directory) {
565 Ok(rel) => rel,
566 Err(_) => {
567 if let Some(alt_target_dir) = &self.alt_target_dir {
579 actual_path.strip_prefix(alt_target_dir).ok()?
580 } else {
581 return None;
582 }
583 }
584 };
585
586 self.rust_build_meta
587 .linked_paths
588 .entry(convert_rel_path_to_forward_slash(rel_path))
589 .or_default()
590 .insert(package_id.repr.clone());
591
592 Some(())
593 }
594
595 fn finish(mut self) -> BinaryList {
596 self.rust_binaries.sort_by(|b1, b2| b1.id.cmp(&b2.id));
597
598 let relevant_package_ids = self
600 .rust_binaries
601 .iter()
602 .map(|bin| bin.package_id.clone())
603 .collect::<HashSet<_>>();
604
605 self.rust_build_meta
606 .build_script_out_dirs
607 .retain(|package_id, _| relevant_package_ids.contains(package_id));
608 if let Some(info) = &mut self.rust_build_meta.build_script_info {
609 info.retain(|package_id, _| relevant_package_ids.contains(package_id));
610 }
611
612 if !self.rust_binaries.is_empty() && self.rust_build_meta.base_output_directories.is_empty()
616 {
617 warn!(
618 target: "nextest-runner::list",
619 "failed to detect any base output directories under the build directory `{}`; \
620 tests that link against dynamic libraries may fail to start. \
621 This usually means Cargo's build directory layout changed -- \
622 please report it at https://github.com/nextest-rs/nextest/issues/new",
623 self.rust_build_meta.build_directory,
624 );
625 }
626
627 BinaryList {
628 rust_build_meta: self.rust_build_meta,
629 rust_binaries: self.rust_binaries,
630 }
631 }
632}
633
634fn artifact_build_platform(
640 artifact_path: &Utf8Path,
641 target_directory: &Utf8Path,
642 build_directory: &Utf8Path,
643 target_triple: Option<&str>,
644) -> Option<BuildPlatform> {
645 let Some(target_triple) = target_triple else {
647 return Some(BuildPlatform::Target);
648 };
649
650 let Some(rel_path) = [target_directory, build_directory]
654 .into_iter()
655 .filter_map(|root| artifact_path.strip_prefix(root).ok())
656 .min_by_key(|rel_path| rel_path.as_str().len())
657 else {
658 debug!(
659 target: "nextest-runner::list",
660 "artifact path `{}` is under neither the target directory `{}` nor the build \
661 directory `{}`, recording its build platform as unknown",
662 artifact_path, target_directory, build_directory,
663 );
664 return None;
665 };
666
667 if rel_path.starts_with(target_triple) {
668 Some(BuildPlatform::Target)
669 } else {
670 Some(BuildPlatform::Host)
671 }
672}
673
674fn base_output_dir(rel_path: &Utf8Path) -> Option<&Utf8Path> {
675 let parent = rel_path.parent()?;
676 let base = match parent.file_name()? {
677 "deps" | "examples" => parent.parent()?,
684 "out" => parent
686 .ancestors()
687 .nth(3)
708 .filter(|dir| dir.file_name() == Some("build"))?
709 .parent()?,
710 _ => return None,
711 };
712
713 (!base.as_str().is_empty()).then_some(base)
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721 use crate::{
722 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
723 list::{
724 BuildScriptInfo, SerializableFormat,
725 test_helpers::{
726 PACKAGE_GRAPH_FIXTURE, PACKAGE_METADATA_ID, WORKSPACE_PACKAGE_ID, package_metadata,
727 },
728 },
729 platform::{HostPlatform, PlatformLibdir, TargetPlatform},
730 };
731 use indoc::indoc;
732 use maplit::btreeset;
733 use nextest_metadata::PlatformLibdirUnavailable;
734 use pretty_assertions::assert_eq;
735 use serde_json::json;
736 use target_spec::{Platform, TargetFeatures};
737
738 #[test]
739 fn test_parse_binary_list() {
740 let fake_bin_test = RustTestBinary {
741 id: "fake-package::bin/fake-binary".into(),
742 path: "/fake/binary".into(),
743 package_id: "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)"
744 .to_owned(),
745 kind: RustTestBinaryKind::LIB,
746 name: "fake-binary".to_owned(),
747 build_platform: BuildPlatform::Target,
748 };
749 let fake_macro_test = RustTestBinary {
750 id: "fake-macro::proc-macro/fake-macro".into(),
751 path: "/fake/macro".into(),
752 package_id: "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)"
753 .to_owned(),
754 kind: RustTestBinaryKind::PROC_MACRO,
755 name: "fake-macro".to_owned(),
756 build_platform: BuildPlatform::Host,
757 };
758
759 let fake_triple = TargetTriple {
760 platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
761 source: TargetTripleSource::CliOption,
762 location: TargetDefinitionLocation::Builtin,
763 };
764 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
765 let build_platforms = BuildPlatforms {
766 host: HostPlatform {
767 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
768 libdir: PlatformLibdir::Available(Utf8PathBuf::from(fake_host_libdir)),
769 },
770 target: Some(TargetPlatform {
771 triple: fake_triple,
772 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
774 }),
775 };
776
777 let mut rust_build_meta =
778 RustBuildMeta::new("/fake/target", "/fake/target", build_platforms);
779 rust_build_meta
782 .base_output_directories
783 .insert("aarch64-unknown-linux-gnu/my-profile".into());
784 rust_build_meta
785 .base_output_directories
786 .insert("my-profile".into());
787 for non_test_binary in [
788 RustNonTestBinarySummary {
789 name: "my-name".into(),
790 kind: RustNonTestBinaryKind::BIN_EXE,
791 path: "aarch64-unknown-linux-gnu/my-profile/my-name".into(),
792 build_platform: Some(BuildPlatform::Target),
793 },
794 RustNonTestBinarySummary {
795 name: "your-name".into(),
796 kind: RustNonTestBinaryKind::DYLIB,
797 path: "my-profile/your-name.dll".into(),
798 build_platform: Some(BuildPlatform::Host),
799 },
800 RustNonTestBinarySummary {
801 name: "your-name".into(),
802 kind: RustNonTestBinaryKind::DYLIB,
803 path: "my-profile/your-name.exp".into(),
804 build_platform: Some(BuildPlatform::Host),
805 },
806 ] {
807 rust_build_meta
808 .non_test_binaries
809 .insert(guppy::PackageId::new("my-package-id"), non_test_binary);
810 }
811
812 let binary_list = BinaryList {
813 rust_build_meta,
814 rust_binaries: vec![fake_bin_test, fake_macro_test],
815 };
816
817 static EXPECTED_HUMAN: &str = indoc! {"
819 fake-package::bin/fake-binary
820 fake-macro::proc-macro/fake-macro
821 "};
822 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {r"
823 fake-package::bin/fake-binary:
824 bin: /fake/binary
825 build platform: target
826 fake-macro::proc-macro/fake-macro:
827 bin: /fake/macro
828 build platform: host
829 "};
830 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
831 {
832 "rust-build-meta": {
833 "target-directory": "/fake/target",
834 "build-directory": "/fake/target",
835 "base-output-directories": [
836 "aarch64-unknown-linux-gnu/my-profile",
837 "my-profile"
838 ],
839 "non-test-binaries": {
840 "my-package-id": [
841 {
842 "name": "my-name",
843 "kind": "bin-exe",
844 "path": "aarch64-unknown-linux-gnu/my-profile/my-name",
845 "build-platform": "target"
846 },
847 {
848 "name": "your-name",
849 "kind": "dylib",
850 "path": "my-profile/your-name.dll",
851 "build-platform": "host"
852 },
853 {
854 "name": "your-name",
855 "kind": "dylib",
856 "path": "my-profile/your-name.exp",
857 "build-platform": "host"
858 }
859 ]
860 },
861 "build-script-out-dirs": {},
862 "build-script-info": {},
863 "linked-paths": [],
864 "platforms": {
865 "host": {
866 "platform": {
867 "triple": "x86_64-unknown-linux-gnu",
868 "target-features": "unknown"
869 },
870 "libdir": {
871 "status": "available",
872 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
873 }
874 },
875 "targets": [
876 {
877 "platform": {
878 "triple": "aarch64-unknown-linux-gnu",
879 "target-features": "unknown"
880 },
881 "libdir": {
882 "status": "unavailable",
883 "reason": "rustc-output-error"
884 }
885 }
886 ]
887 },
888 "target-platforms": [
889 {
890 "triple": "aarch64-unknown-linux-gnu",
891 "target-features": "unknown"
892 }
893 ],
894 "target-platform": "aarch64-unknown-linux-gnu"
895 },
896 "rust-binaries": {
897 "fake-macro::proc-macro/fake-macro": {
898 "binary-id": "fake-macro::proc-macro/fake-macro",
899 "binary-name": "fake-macro",
900 "package-id": "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)",
901 "kind": "proc-macro",
902 "binary-path": "/fake/macro",
903 "build-platform": "host"
904 },
905 "fake-package::bin/fake-binary": {
906 "binary-id": "fake-package::bin/fake-binary",
907 "binary-name": "fake-binary",
908 "package-id": "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)",
909 "kind": "lib",
910 "binary-path": "/fake/binary",
911 "build-platform": "target"
912 }
913 }
914 }"#};
915 static EXPECTED_ONELINE: &str = indoc! {"
917 fake-package::bin/fake-binary
918 fake-macro::proc-macro/fake-macro
919 "};
920 static EXPECTED_ONELINE_VERBOSE: &str = indoc! {r"
921 fake-package::bin/fake-binary [bin: /fake/binary] [build platform: target]
922 fake-macro::proc-macro/fake-macro [bin: /fake/macro] [build platform: host]
923 "};
924
925 assert_eq!(
926 binary_list
927 .to_string(OutputFormat::Human { verbose: false })
928 .expect("human succeeded"),
929 EXPECTED_HUMAN
930 );
931 assert_eq!(
932 binary_list
933 .to_string(OutputFormat::Human { verbose: true })
934 .expect("human succeeded"),
935 EXPECTED_HUMAN_VERBOSE
936 );
937 assert_eq!(
938 binary_list
939 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
940 .expect("json-pretty succeeded"),
941 EXPECTED_JSON_PRETTY
942 );
943 assert_eq!(
944 binary_list
945 .to_string(OutputFormat::Oneline { verbose: false })
946 .expect("oneline succeeded"),
947 EXPECTED_ONELINE
948 );
949 assert_eq!(
950 binary_list
951 .to_string(OutputFormat::Oneline { verbose: true })
952 .expect("oneline verbose succeeded"),
953 EXPECTED_ONELINE_VERBOSE
954 );
955 }
956
957 #[test]
958 fn test_parse_binary_list_from_message_lines() {
959 let build_platforms = BuildPlatforms {
960 host: HostPlatform {
961 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
962 libdir: PlatformLibdir::Available("/fake/libdir".into()),
963 },
964 target: None,
965 };
966 let package = package_metadata();
967 let artifact_path = PACKAGE_GRAPH_FIXTURE
971 .workspace()
972 .build_directory()
973 .expect("fixture sets build_directory")
974 .join("debug/deps/metadata_helper-test");
975 let compiler_artifact = artifact_json(
976 package.name(),
977 &["lib"],
978 std::slice::from_ref(&artifact_path),
979 Some(&artifact_path),
980 TestTarget::Yes,
981 );
982 let input = format!("this is not JSON\n{}\n\n", compiler_artifact);
983
984 let from_messages = BinaryList::from_messages(
985 input.as_bytes(),
986 &PACKAGE_GRAPH_FIXTURE,
987 build_platforms.clone(),
988 )
989 .expect("parsing from messages succeeds");
990
991 let mut builder = BinaryListBuilder::new(&PACKAGE_GRAPH_FIXTURE, build_platforms);
992 for line in input.lines() {
993 builder
994 .process_message_line(line)
995 .expect("processing line succeeds");
996 }
997 let from_lines = builder.finish();
998
999 assert_eq!(
1000 from_lines.rust_build_meta.base_output_directories,
1001 btreeset! { Utf8PathBuf::from("debug") },
1002 "base output directory detected from the artifact's executable path"
1003 );
1004
1005 assert_eq!(
1006 from_lines
1007 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1008 .expect("json-pretty succeeds"),
1009 from_messages
1010 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
1011 .expect("json-pretty succeeds")
1012 );
1013 }
1014
1015 #[test]
1016 fn test_artifact_build_platform() {
1017 static TARGET_DIR: &str = "/w/target";
1018 static TRIPLE: &str = "aarch64-unknown-linux-gnu";
1019
1020 struct Case {
1021 artifact_path: &'static str,
1022 build_directory: Option<&'static str>,
1025 target_triple: Option<&'static str>,
1026 expected: Option<BuildPlatform>,
1027 description: &'static str,
1028 }
1029
1030 let cases = [
1031 Case {
1032 artifact_path: "/w/target/debug/libfoo.so",
1033 build_directory: None,
1034 target_triple: None,
1035 expected: Some(BuildPlatform::Target),
1036 description: "with no target platform the host and the target coincide, and \
1037 nextest reports target",
1038 },
1039 Case {
1040 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1041 build_directory: None,
1042 target_triple: Some(TRIPLE),
1043 expected: Some(BuildPlatform::Target),
1044 description: "a dylib uplifted into the target platform's artifact directory",
1045 },
1046 Case {
1047 artifact_path: "/w/target/debug/libfoo.so",
1048 build_directory: None,
1049 target_triple: Some(TRIPLE),
1050 expected: Some(BuildPlatform::Host),
1051 description: "the same dylib, built for the host as a build dependency",
1052 },
1053 Case {
1054 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib",
1055 build_directory: None,
1056 target_triple: Some(TRIPLE),
1057 expected: Some(BuildPlatform::Target),
1058 description: "an rlib that was never uplifted, legacy layout",
1059 },
1060 Case {
1061 artifact_path: "/w/target/debug/deps/libfoo.rlib",
1062 build_directory: None,
1063 target_triple: Some(TRIPLE),
1064 expected: Some(BuildPlatform::Host),
1065 description: "the same rlib built for the host, legacy layout",
1066 },
1067 Case {
1068 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1069 build_directory: None,
1070 target_triple: Some(TRIPLE),
1071 expected: Some(BuildPlatform::Target),
1072 description: "an rlib that was never uplifted, build-dir layout v2",
1073 },
1074 Case {
1075 artifact_path: "/w/target/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1076 build_directory: None,
1077 target_triple: Some(TRIPLE),
1078 expected: Some(BuildPlatform::Host),
1079 description: "the same rlib built for the host, build-dir layout v2",
1080 },
1081 Case {
1082 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1083 build_directory: Some("/w/build"),
1084 target_triple: Some(TRIPLE),
1085 expected: Some(BuildPlatform::Target),
1086 description: "a build directory beside the target directory: uplifts are unaffected",
1087 },
1088 Case {
1089 artifact_path: "/w/target/build/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib",
1090 build_directory: Some("/w/target/build"),
1091 target_triple: Some(TRIPLE),
1092 expected: Some(BuildPlatform::Target),
1093 description: "a build directory inside the target directory: the build directory is the \
1094 innermost root, so the triple is still the first component under it",
1095 },
1096 Case {
1097 artifact_path: "/w/target/build/debug/deps/libfoo.rlib",
1098 build_directory: Some("/w/target/build"),
1099 target_triple: Some(TRIPLE),
1100 expected: Some(BuildPlatform::Host),
1101 description: "a build directory inside the target directory, host artifact",
1102 },
1103 Case {
1104 artifact_path: "/w/target/build/aarch64-unknown-linux-gnu/debug/build/foo/9e6e6f7b/out/libfoo.rlib",
1105 build_directory: Some("/w/target/build"),
1106 target_triple: Some(TRIPLE),
1107 expected: Some(BuildPlatform::Target),
1108 description: "the shape Cargo emits under the build-dir v2 layout for \
1109 `build.build-dir = target/build` plus --target: nesting and v2 at once. \
1110 Verified by hand against a real nightly build",
1111 },
1112 Case {
1113 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1114 build_directory: Some("/w/target/build"),
1115 target_triple: Some(TRIPLE),
1116 expected: Some(BuildPlatform::Target),
1117 description: "a build directory inside the target directory: uplifts still go to the \
1118 target directory, which is the innermost root containing them",
1119 },
1120 Case {
1121 artifact_path: "/w/target/debug/libfoo.so",
1122 build_directory: Some("/w/target/build"),
1123 target_triple: Some(TRIPLE),
1124 expected: Some(BuildPlatform::Host),
1125 description: "a host dylib uplifted alongside a nested build directory. The \
1126 nested build directory does not contain it, so dropping the target directory \
1127 from the candidate roots would leave nothing to strip",
1128 },
1129 Case {
1130 artifact_path: "/w/target/debug/deps/aarch64-unknown-linux-gnu/libfoo.so",
1131 build_directory: None,
1132 target_triple: Some(TRIPLE),
1133 expected: Some(BuildPlatform::Host),
1134 description: "only the first component is the triple slot; a triple-named \
1135 directory deeper in the path means nothing",
1136 },
1137 Case {
1138 artifact_path: "/w/target/debug/libfoo.so",
1139 build_directory: Some("/w"),
1140 target_triple: Some(TRIPLE),
1141 expected: Some(BuildPlatform::Host),
1142 description: "the reverse nesting: `build.build-dir` and `build.target-dir` are \
1143 independent, so the target directory can sit inside the build directory. It \
1144 is then the innermost root",
1145 },
1146 Case {
1147 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1148 build_directory: Some("/w"),
1149 target_triple: Some(TRIPLE),
1150 expected: Some(BuildPlatform::Target),
1151 description: "the reverse nesting, target artifact: resolving against the outer \
1152 build directory would see `target` first and misreport this as host",
1153 },
1154 Case {
1155 artifact_path: "/w/target/aarch64-unknown-linux-gnu-ilp32/debug/libfoo.so",
1156 build_directory: None,
1157 target_triple: Some(TRIPLE),
1158 expected: Some(BuildPlatform::Host),
1159 description: "a directory whose name merely starts with the triple is not the triple",
1160 },
1161 Case {
1162 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1163 build_directory: Some("/w/target/aarch"),
1164 target_triple: Some(TRIPLE),
1165 expected: Some(BuildPlatform::Target),
1166 description: "root selection is component-wise, so a build directory that is only \
1167 a string prefix of the path never wins; a byte-wise prefix test would strip \
1168 `/w/target/aarch` here and misread the artifact as host",
1169 },
1170 Case {
1171 artifact_path: "/w/target/my-custom-target/debug/libfoo.so",
1172 build_directory: None,
1173 target_triple: Some("my-custom-target"),
1174 expected: Some(BuildPlatform::Target),
1175 description: "a custom JSON target's directory is its file stem, which is also its triple \
1176 string in nextest",
1177 },
1178 Case {
1179 artifact_path: "/w/target/aarch64-unknown-linux-gnu/libfoo.so",
1180 build_directory: None,
1181 target_triple: Some(TRIPLE),
1182 expected: Some(BuildPlatform::Target),
1183 description: "KNOWN WRONG, pinned so the limitation stays visible: a custom \
1184 profile named exactly after the triple puts host uplifts at \
1185 `<profile>/libfoo.so` under the target directory, and the profile component \
1186 occupies the `<triple>` slot, so a host artifact reads as target. \
1187 Self-inflicted",
1188 },
1189 Case {
1190 artifact_path: "/w/target/aarch64-unknown-linux-gnu/debug/libfoo.so",
1191 build_directory: Some("/w/target/aarch64-unknown-linux-gnu"),
1192 target_triple: Some(TRIPLE),
1193 expected: Some(BuildPlatform::Host),
1194 description: "KNOWN WRONG, pinned so the limitation stays visible: a build \
1195 directory nested in the target directory and named after the triple becomes \
1196 the innermost root and swallows the triple component, so an uplifted target \
1197 artifact reads as host. Self-inflicted, like the triple-named custom profile \
1198 above",
1199 },
1200 Case {
1201 artifact_path: "/elsewhere/debug/libfoo.so",
1202 build_directory: None,
1203 target_triple: Some(TRIPLE),
1204 expected: None,
1205 description: "a path under neither root has no `<triple>` slot to read, so the \
1206 platform is unknown",
1207 },
1208 ];
1209
1210 for case in cases {
1211 let build_directory = case.build_directory.unwrap_or(TARGET_DIR);
1212 assert_eq!(
1213 artifact_build_platform(
1214 Utf8Path::new(case.artifact_path),
1215 Utf8Path::new(TARGET_DIR),
1216 Utf8Path::new(build_directory),
1217 case.target_triple,
1218 ),
1219 case.expected,
1220 "{}: build platform for {} (build directory {build_directory})",
1221 case.description,
1222 case.artifact_path,
1223 );
1224 }
1225 }
1226
1227 fn cross_build_platforms() -> BuildPlatforms {
1228 let target_triple = TargetTriple {
1229 platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown)
1230 .expect("aarch64-unknown-linux-gnu is a builtin triple"),
1231 source: TargetTripleSource::CliOption,
1232 location: TargetDefinitionLocation::Builtin,
1233 };
1234 BuildPlatforms {
1235 host: HostPlatform {
1236 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
1237 libdir: PlatformLibdir::Available("/fake/host/libdir".into()),
1238 },
1239 target: Some(TargetPlatform::new(
1240 target_triple,
1241 PlatformLibdir::Available("/fake/target/libdir".into()),
1242 )),
1243 }
1244 }
1245
1246 #[test]
1247 fn test_non_test_build_platform_resolves_against_the_build_directory() {
1248 let mut state = BinaryListBuildState::new(&PACKAGE_GRAPH_FIXTURE, cross_build_platforms());
1249 state.rust_build_meta.target_directory = "/w/target".into();
1250 state.rust_build_meta.build_directory = "/w/target/build".into();
1251
1252 assert_eq!(
1253 state.non_test_build_platform(Utf8Path::new(
1254 "/w/target/build/aarch64-unknown-linux-gnu/debug/deps/libfoo.rlib"
1255 )),
1256 Some(BuildPlatform::Target),
1257 "the nested build directory is the innermost root, so the triple is the first \
1258 component under it; resolving against the target directory instead would see \
1259 `build` first and misreport this as host"
1260 );
1261 }
1262
1263 #[test]
1264 fn test_non_test_binaries_record_the_build_platform() {
1265 let build_platforms = cross_build_platforms();
1266
1267 let workspace = PACKAGE_GRAPH_FIXTURE.workspace();
1268 let target_dir = workspace.target_directory();
1269 let build_dir = workspace
1270 .build_directory()
1271 .expect("fixture sets build_directory");
1272
1273 let input = [
1280 dylib_artifact_json(
1281 "shared",
1282 &[
1283 target_dir.join("debug/libshared.so"),
1284 build_dir.join("debug/deps/libshared.rlib"),
1285 ],
1286 ),
1287 dylib_artifact_json(
1288 "shared",
1289 &[
1290 target_dir.join("aarch64-unknown-linux-gnu/debug/libshared.so"),
1291 build_dir.join("aarch64-unknown-linux-gnu/debug/deps/libshared.rlib"),
1292 ],
1293 ),
1294 bin_artifact_json(
1295 "mainbin",
1296 &target_dir.join("aarch64-unknown-linux-gnu/debug/mainbin"),
1297 ),
1298 ]
1299 .map(|artifact| artifact.to_string())
1300 .join("\n");
1301
1302 let binary_list =
1303 BinaryList::from_messages(input.as_bytes(), &PACKAGE_GRAPH_FIXTURE, build_platforms)
1304 .expect("parsing from messages succeeds");
1305
1306 assert_eq!(
1307 binary_list.rust_build_meta.non_test_binaries.to_summary(),
1308 BTreeMap::from([(
1309 PACKAGE_METADATA_ID.to_owned(),
1310 btreeset! {
1311 RustNonTestBinarySummary {
1312 name: "mainbin".to_owned(),
1313 kind: RustNonTestBinaryKind::BIN_EXE,
1314 path: "aarch64-unknown-linux-gnu/debug/mainbin".into(),
1315 build_platform: Some(BuildPlatform::Target),
1316 },
1317 RustNonTestBinarySummary {
1318 name: "shared".to_owned(),
1319 kind: RustNonTestBinaryKind::DYLIB,
1320 path: "aarch64-unknown-linux-gnu/debug/libshared.so".into(),
1321 build_platform: Some(BuildPlatform::Target),
1322 },
1323 RustNonTestBinarySummary {
1324 name: "shared".to_owned(),
1325 kind: RustNonTestBinaryKind::DYLIB,
1326 path: "debug/libshared.so".into(),
1327 build_platform: Some(BuildPlatform::Host),
1328 },
1329 },
1330 )]),
1331 "the two builds of one dylib target are told apart by the triple component of their \
1332 uplifted paths; the rlibs live in the fixture's build directory, which is not under \
1333 the target directory, so they are not recorded at all"
1334 );
1335 }
1336
1337 #[test]
1338 fn test_build_scripts_recorded_only_for_workspace_packages() {
1339 let build_dir = PACKAGE_GRAPH_FIXTURE
1340 .workspace()
1341 .build_directory()
1342 .expect("fixture sets build_directory");
1343 let std_id = "path+file:///fake/sysroot/lib/rustlib/src/rust/library/std#0.0.0";
1344
1345 let build_platforms =
1346 BuildPlatforms::new_with_no_target().expect("host platform is detected");
1347 let mut state = BinaryListBuildState::new(&PACKAGE_GRAPH_FIXTURE, build_platforms);
1348 for (package_id, name) in [
1349 (WORKSPACE_PACKAGE_ID, "workspace"),
1350 (PACKAGE_METADATA_ID, "non-workspace"),
1351 (std_id, "not-in-graph"),
1352 ] {
1353 let linked_path = build_dir.join(format!("debug/build/{name}-link"));
1354 let message = json!({
1355 "reason": "build-script-executed",
1356 "package_id": package_id,
1357 "linked_libs": [],
1358 "linked_paths": [format!("native={linked_path}")],
1359 "cfgs": [],
1360 "env": [["BUILD_SCRIPT_ENV", name]],
1361 "out_dir": build_dir.join(format!("debug/build/{name}/out")),
1362 });
1363 let message = serde_json::from_value::<Message>(message)
1364 .expect("message is a valid build-script-executed message");
1365 state
1366 .process_message(message)
1367 .expect("processing build script message succeeds");
1368 }
1369
1370 let expected_out_dirs = BTreeMap::from([(
1371 WORKSPACE_PACKAGE_ID.to_owned(),
1372 Utf8PathBuf::from("debug/build/workspace/out"),
1373 )]);
1374 let expected_info = BTreeMap::from([(
1375 WORKSPACE_PACKAGE_ID.to_owned(),
1376 BuildScriptInfo {
1377 envs: BTreeMap::from([("BUILD_SCRIPT_ENV".to_owned(), "workspace".to_owned())]),
1378 },
1379 )]);
1380 let expected_linked_paths = BTreeMap::from([
1381 (
1382 Utf8PathBuf::from("debug/build/workspace-link"),
1383 btreeset! { WORKSPACE_PACKAGE_ID.to_owned() },
1384 ),
1385 (
1386 Utf8PathBuf::from("debug/build/non-workspace-link"),
1387 btreeset! { PACKAGE_METADATA_ID.to_owned() },
1388 ),
1389 (
1390 Utf8PathBuf::from("debug/build/not-in-graph-link"),
1391 btreeset! { std_id.to_owned() },
1392 ),
1393 ]);
1394
1395 let meta = &state.rust_build_meta;
1396 assert_eq!(
1397 meta.build_script_out_dirs, expected_out_dirs,
1398 "only the workspace package has its out_dir recorded"
1399 );
1400 assert_eq!(
1401 meta.build_script_info,
1402 Some(expected_info),
1403 "only the workspace package has its env recorded"
1404 );
1405 assert_eq!(
1406 meta.linked_paths, expected_linked_paths,
1407 "linked paths are recorded for every package, including ones absent from \
1408 cargo metadata (e.g. standard library crates built with -Zbuild-std)"
1409 );
1410 }
1411
1412 #[derive(Clone, Copy)]
1413 enum TestTarget {
1414 Yes,
1415 No,
1416 }
1417
1418 impl TestTarget {
1419 fn as_bool(self) -> bool {
1420 match self {
1421 Self::Yes => true,
1422 Self::No => false,
1423 }
1424 }
1425 }
1426
1427 fn bin_artifact_json(name: &str, path: &Utf8Path) -> serde_json::Value {
1428 artifact_json(
1429 name,
1430 &["bin"],
1431 &[path.to_owned()],
1432 Some(path),
1433 TestTarget::No,
1434 )
1435 }
1436
1437 fn dylib_artifact_json(name: &str, filenames: &[Utf8PathBuf]) -> serde_json::Value {
1438 artifact_json(name, &["dylib", "rlib"], filenames, None, TestTarget::No)
1439 }
1440
1441 fn artifact_json(
1442 name: &str,
1443 kind: &[&str],
1444 filenames: &[Utf8PathBuf],
1445 executable: Option<&Utf8Path>,
1446 test_target: TestTarget,
1447 ) -> serde_json::Value {
1448 let package = package_metadata();
1449 let src_path = package
1450 .manifest_path()
1451 .parent()
1452 .expect("manifest path has a parent")
1453 .join("src/lib.rs");
1454 let is_test = test_target.as_bool();
1455
1456 json!({
1457 "reason": "compiler-artifact",
1458 "package_id": PACKAGE_METADATA_ID,
1459 "manifest_path": package.manifest_path(),
1460 "target": {
1461 "name": name,
1462 "kind": kind,
1463 "crate_types": kind,
1464 "required-features": [],
1465 "src_path": src_path,
1466 "edition": "2021",
1467 "doctest": is_test,
1468 "test": is_test,
1469 "doc": is_test
1470 },
1471 "profile": {
1472 "opt_level": "0",
1473 "debuginfo": 0,
1474 "debug_assertions": true,
1475 "overflow_checks": true,
1476 "test": is_test
1477 },
1478 "features": [],
1479 "filenames": filenames,
1480 "executable": executable,
1481 "fresh": false
1482 })
1483 }
1484
1485 #[test]
1486 fn test_base_output_dir() {
1487 let cases: &[(&str, Option<&str>, &str)] = &[
1488 ("debug/deps/foo-9e6e6f7b", Some("debug"), "legacy layout"),
1489 (
1490 "aarch64-unknown-linux-gnu/debug/deps/foo-9e6e6f7b",
1491 Some("aarch64-unknown-linux-gnu/debug"),
1492 "legacy layout with a target triple",
1493 ),
1494 (
1495 "debug/build/metadata-helper/9e6e6f7b/out/foo",
1496 Some("debug"),
1497 "v2 layout",
1498 ),
1499 (
1500 "aarch64-unknown-linux-gnu/debug/build/metadata-helper/9e6e6f7b/out/foo",
1501 Some("aarch64-unknown-linux-gnu/debug"),
1502 "v2 layout with a target triple",
1503 ),
1504 (
1505 "debug/build/build/9e6e6f7b/out/foo",
1506 Some("debug"),
1507 "v2 layout, package named build",
1508 ),
1509 (
1510 "debug/examples/foo-9e6e6f7b",
1511 Some("debug"),
1512 "legacy layout, example test binary",
1513 ),
1514 (
1515 "aarch64-unknown-linux-gnu/debug/examples/foo-9e6e6f7b",
1516 Some("aarch64-unknown-linux-gnu/debug"),
1517 "legacy layout, example test binary with a target triple",
1518 ),
1519 (
1520 "debug/build/metadata-helper-9e6e6f7b/out/foo",
1521 None,
1522 "legacy build script out dir",
1523 ),
1524 (
1525 "aarch64-unknown-linux-gnu/debug/build/metadata-helper-9e6e6f7b/out/foo",
1526 None,
1527 "legacy build script out dir with a target triple",
1528 ),
1529 ("debug/foo", None, "uplifted into the profile directory"),
1530 (
1531 "out/foo",
1532 None,
1533 "out directory directly under the build directory",
1534 ),
1535 (
1536 "deps/foo",
1537 None,
1538 "deps directory directly under the build directory",
1539 ),
1540 (
1541 "examples/foo",
1542 None,
1543 "examples directory directly under the build directory",
1544 ),
1545 (
1546 "build/metadata-helper/9e6e6f7b/out/foo",
1547 None,
1548 "v2 build directory directly under the build directory",
1549 ),
1550 ];
1551
1552 for (rel_artifact_path, expected, description) in cases {
1553 assert_eq!(
1554 base_output_dir(Utf8Path::new(rel_artifact_path)),
1555 expected.map(Utf8Path::new),
1556 "{description}: base output dir for artifact path {rel_artifact_path}"
1557 );
1558 }
1559 }
1560}