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 std::{collections::HashSet, io};
20use tracing::warn;
21
22#[derive(Clone, Debug)]
24pub struct RustTestBinary {
25 pub id: RustBinaryId,
27 pub path: Utf8PathBuf,
29 pub package_id: String,
31 pub kind: RustTestBinaryKind,
33 pub name: String,
35 pub build_platform: BuildPlatform,
38}
39
40#[derive(Clone, Debug)]
42pub struct BinaryList {
43 pub rust_build_meta: RustBuildMeta<BinaryListState>,
45
46 pub rust_binaries: Vec<RustTestBinary>,
48}
49
50impl BinaryList {
51 pub fn from_messages(
53 reader: impl io::BufRead,
54 graph: &PackageGraph,
55 build_platforms: BuildPlatforms,
56 ) -> Result<Self, FromMessagesError> {
57 let mut state = BinaryListBuildState::new(graph, build_platforms);
58
59 for message in Message::parse_stream(reader) {
60 let message = message.map_err(FromMessagesError::ReadMessages)?;
61 state.process_message(message)?;
62 }
63
64 Ok(state.finish())
65 }
66
67 pub fn from_summary(summary: BinaryListSummary) -> Result<Self, RustBuildMetaParseError> {
69 let rust_binaries = summary
70 .rust_binaries
71 .into_values()
72 .map(|bin| RustTestBinary {
73 name: bin.binary_name,
74 path: bin.binary_path,
75 package_id: bin.package_id,
76 kind: bin.kind,
77 id: bin.binary_id,
78 build_platform: bin.build_platform,
79 })
80 .collect();
81 Ok(Self {
82 rust_build_meta: RustBuildMeta::from_summary(summary.rust_build_meta)?,
83 rust_binaries,
84 })
85 }
86
87 pub fn write(
89 &self,
90 output_format: OutputFormat,
91 writer: &mut dyn WriteStr,
92 colorize: bool,
93 ) -> Result<(), WriteTestListError> {
94 match output_format {
95 OutputFormat::Human { verbose } => self
96 .write_human(writer, verbose, colorize)
97 .map_err(WriteTestListError::Io),
98 OutputFormat::Oneline { verbose } => self
99 .write_oneline(writer, verbose, colorize)
100 .map_err(WriteTestListError::Io),
101 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
102 }
103 }
104
105 fn to_summary(&self) -> BinaryListSummary {
106 let rust_binaries = self
107 .rust_binaries
108 .iter()
109 .map(|bin| {
110 let summary = RustTestBinarySummary {
111 binary_name: bin.name.clone(),
112 package_id: bin.package_id.clone(),
113 kind: bin.kind.clone(),
114 binary_path: bin.path.clone(),
115 binary_id: bin.id.clone(),
116 build_platform: bin.build_platform,
117 };
118 (bin.id.clone(), summary)
119 })
120 .collect();
121
122 BinaryListSummary {
123 rust_build_meta: self.rust_build_meta.to_summary(),
124 rust_binaries,
125 }
126 }
127
128 fn write_human(
129 &self,
130 writer: &mut dyn WriteStr,
131 verbose: bool,
132 colorize: bool,
133 ) -> io::Result<()> {
134 let mut styles = Styles::default();
135 if colorize {
136 styles.colorize();
137 }
138 for bin in &self.rust_binaries {
139 if verbose {
140 writeln!(writer, "{}:", bin.id.style(styles.binary_id))?;
141 writeln!(writer, " {} {}", "bin:".style(styles.field), bin.path)?;
142 writeln!(
143 writer,
144 " {} {}",
145 "build platform:".style(styles.field),
146 bin.build_platform,
147 )?;
148 } else {
149 writeln!(writer, "{}", bin.id.style(styles.binary_id))?;
150 }
151 }
152 Ok(())
153 }
154
155 fn write_oneline(
156 &self,
157 writer: &mut dyn WriteStr,
158 verbose: bool,
159 colorize: bool,
160 ) -> io::Result<()> {
161 let mut styles = Styles::default();
162 if colorize {
163 styles.colorize();
164 }
165 for bin in &self.rust_binaries {
166 write!(writer, "{}", bin.id.style(styles.binary_id))?;
167 if verbose {
168 write!(
169 writer,
170 " [{}{}] [{}{}]",
171 "bin: ".style(styles.field),
172 bin.path,
173 "build platform: ".style(styles.field),
174 bin.build_platform,
175 )?;
176 }
177 writeln!(writer)?;
178 }
179 Ok(())
180 }
181
182 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
184 let mut s = String::with_capacity(1024);
185 self.write(output_format, &mut s, false)?;
186 Ok(s)
187 }
188}
189
190#[derive(Debug)]
191struct BinaryListBuildState<'g> {
192 graph: &'g PackageGraph,
193 rust_binaries: Vec<RustTestBinary>,
194 rust_build_meta: RustBuildMeta<BinaryListState>,
195 alt_target_dir: Option<Utf8PathBuf>,
196}
197
198impl<'g> BinaryListBuildState<'g> {
199 fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
200 let rust_target_dir = graph.workspace().target_directory().to_path_buf();
201 let alt_target_dir = std::env::var("__NEXTEST_ALT_TARGET_DIR")
203 .ok()
204 .map(Utf8PathBuf::from);
205
206 Self {
207 graph,
208 rust_binaries: vec![],
209 rust_build_meta: RustBuildMeta::new(rust_target_dir, build_platforms),
210 alt_target_dir,
211 }
212 }
213
214 fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
215 match message {
216 Message::CompilerArtifact(artifact) => {
217 self.process_artifact(artifact)?;
218 }
219 Message::BuildScriptExecuted(build_script) => {
220 self.process_build_script(build_script)?;
221 }
222 _ => {
223 }
225 }
226
227 Ok(())
228 }
229
230 fn process_artifact(&mut self, artifact: Artifact) -> Result<(), FromMessagesError> {
231 if let Some(path) = artifact.executable {
232 self.detect_base_output_dir(&path);
233
234 if artifact.profile.test {
235 let package_id = artifact.package_id.repr;
236
237 let name = artifact.target.name;
240
241 let package = self
242 .graph
243 .metadata(&guppy::PackageId::new(package_id.clone()))
244 .map_err(FromMessagesError::PackageGraph)?;
245
246 let kind = artifact.target.kind;
247 if kind.is_empty() {
248 return Err(FromMessagesError::MissingTargetKind {
249 package_name: package.name().to_owned(),
250 binary_name: name.clone(),
251 });
252 }
253
254 let (computed_kind, platform) = if kind.iter().any(|k| {
255 matches!(
257 k,
258 TargetKind::Lib
259 | TargetKind::RLib
260 | TargetKind::DyLib
261 | TargetKind::CDyLib
262 | TargetKind::StaticLib
263 )
264 }) {
265 (RustTestBinaryKind::LIB, BuildPlatform::Target)
266 } else if let Some(TargetKind::ProcMacro) = kind.first() {
267 (RustTestBinaryKind::PROC_MACRO, BuildPlatform::Host)
268 } else {
269 (
271 RustTestBinaryKind::new(
272 kind.into_iter()
273 .next()
274 .expect("already checked that kind is non-empty")
275 .to_string(),
276 ),
277 BuildPlatform::Target,
278 )
279 };
280
281 let id = RustBinaryId::from_parts(package.name(), &computed_kind, &name);
283
284 self.rust_binaries.push(RustTestBinary {
285 path,
286 package_id,
287 kind: computed_kind,
288 name,
289 id,
290 build_platform: platform,
291 });
292 } else if artifact
293 .target
294 .kind
295 .iter()
296 .any(|x| matches!(x, TargetKind::Bin))
297 {
298 if let Ok(rel_path) = path.strip_prefix(&self.rust_build_meta.target_directory) {
302 let non_test_binary = RustNonTestBinarySummary {
303 name: artifact.target.name,
304 kind: RustNonTestBinaryKind::BIN_EXE,
305 path: convert_rel_path_to_forward_slash(rel_path),
306 };
307
308 self.rust_build_meta
309 .non_test_binaries
310 .entry(artifact.package_id.repr)
311 .or_default()
312 .insert(non_test_binary);
313 };
314 }
315 } else if artifact
316 .target
317 .kind
318 .iter()
319 .any(|x| matches!(x, TargetKind::DyLib | TargetKind::CDyLib))
320 {
321 for filename in artifact.filenames {
323 if let Ok(rel_path) = filename.strip_prefix(&self.rust_build_meta.target_directory)
324 {
325 let non_test_binary = RustNonTestBinarySummary {
326 name: artifact.target.name.clone(),
327 kind: RustNonTestBinaryKind::DYLIB,
328 path: convert_rel_path_to_forward_slash(rel_path),
329 };
330 self.rust_build_meta
331 .non_test_binaries
332 .entry(artifact.package_id.repr.clone())
333 .or_default()
334 .insert(non_test_binary);
335 }
336 }
337 }
338
339 Ok(())
340 }
341
342 fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
354 let rel_path = artifact_path
356 .strip_prefix(&self.rust_build_meta.target_directory)
357 .ok()?;
358 let parent = rel_path.parent()?;
359 if parent.file_name() == Some("deps") {
360 let base = parent.parent()?;
361 if !self.rust_build_meta.base_output_directories.contains(base) {
362 self.rust_build_meta
363 .base_output_directories
364 .insert(convert_rel_path_to_forward_slash(base));
365 }
366 }
367 Some(())
368 }
369
370 fn process_build_script(&mut self, build_script: BuildScript) -> Result<(), FromMessagesError> {
371 for path in build_script.linked_paths {
372 self.detect_linked_path(&build_script.package_id, &path);
373 }
374
375 let package_id = guppy::PackageId::new(build_script.package_id.repr);
377 let in_workspace = self.graph.metadata(&package_id).map_or_else(
378 |_| {
379 warn!(
381 target: "nextest-runner::list",
382 "warning: saw package ID `{}` which wasn't produced by cargo metadata",
383 package_id
384 );
385 false
386 },
387 |p| p.in_workspace(),
388 );
389 if in_workspace {
390 if let Ok(rel_out_dir) = build_script
392 .out_dir
393 .strip_prefix(&self.rust_build_meta.target_directory)
394 {
395 self.rust_build_meta.build_script_out_dirs.insert(
396 package_id.repr().to_owned(),
397 convert_rel_path_to_forward_slash(rel_out_dir),
398 );
399 }
400 }
401
402 Ok(())
403 }
404
405 fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
407 let actual_path = match path.as_str().split_once('=') {
409 Some((_, p)) => p.into(),
410 None => path,
411 };
412
413 let rel_path = match actual_path.strip_prefix(&self.rust_build_meta.target_directory) {
414 Ok(rel) => rel,
415 Err(_) => {
416 if let Some(alt_target_dir) = &self.alt_target_dir {
428 actual_path.strip_prefix(alt_target_dir).ok()?
429 } else {
430 return None;
431 }
432 }
433 };
434
435 self.rust_build_meta
436 .linked_paths
437 .entry(convert_rel_path_to_forward_slash(rel_path))
438 .or_default()
439 .insert(package_id.repr.clone());
440
441 Some(())
442 }
443
444 fn finish(mut self) -> BinaryList {
445 self.rust_binaries.sort_by(|b1, b2| b1.id.cmp(&b2.id));
446
447 let relevant_package_ids = self
449 .rust_binaries
450 .iter()
451 .map(|bin| bin.package_id.clone())
452 .collect::<HashSet<_>>();
453
454 self.rust_build_meta
455 .build_script_out_dirs
456 .retain(|package_id, _| relevant_package_ids.contains(package_id));
457
458 BinaryList {
459 rust_build_meta: self.rust_build_meta,
460 rust_binaries: self.rust_binaries,
461 }
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::{
469 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
470 list::SerializableFormat,
471 platform::{HostPlatform, PlatformLibdir, TargetPlatform},
472 };
473 use indoc::indoc;
474 use maplit::btreeset;
475 use nextest_metadata::PlatformLibdirUnavailable;
476 use pretty_assertions::assert_eq;
477 use target_spec::{Platform, TargetFeatures};
478
479 #[test]
480 fn test_parse_binary_list() {
481 let fake_bin_test = RustTestBinary {
482 id: "fake-package::bin/fake-binary".into(),
483 path: "/fake/binary".into(),
484 package_id: "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)"
485 .to_owned(),
486 kind: RustTestBinaryKind::LIB,
487 name: "fake-binary".to_owned(),
488 build_platform: BuildPlatform::Target,
489 };
490 let fake_macro_test = RustTestBinary {
491 id: "fake-macro::proc-macro/fake-macro".into(),
492 path: "/fake/macro".into(),
493 package_id: "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)"
494 .to_owned(),
495 kind: RustTestBinaryKind::PROC_MACRO,
496 name: "fake-macro".to_owned(),
497 build_platform: BuildPlatform::Host,
498 };
499
500 let fake_triple = TargetTriple {
501 platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
502 source: TargetTripleSource::CliOption,
503 location: TargetDefinitionLocation::Builtin,
504 };
505 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
506 let build_platforms = BuildPlatforms {
507 host: HostPlatform {
508 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
509 libdir: PlatformLibdir::Available(Utf8PathBuf::from(fake_host_libdir)),
510 },
511 target: Some(TargetPlatform {
512 triple: fake_triple,
513 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
515 }),
516 };
517
518 let mut rust_build_meta = RustBuildMeta::new("/fake/target", build_platforms);
519 rust_build_meta
520 .base_output_directories
521 .insert("my-profile".into());
522 rust_build_meta.non_test_binaries.insert(
523 "my-package-id".into(),
524 btreeset! {
525 RustNonTestBinarySummary {
526 name: "my-name".into(),
527 kind: RustNonTestBinaryKind::BIN_EXE,
528 path: "my-profile/my-name".into(),
529 },
530 RustNonTestBinarySummary {
531 name: "your-name".into(),
532 kind: RustNonTestBinaryKind::DYLIB,
533 path: "my-profile/your-name.dll".into(),
534 },
535 RustNonTestBinarySummary {
536 name: "your-name".into(),
537 kind: RustNonTestBinaryKind::DYLIB,
538 path: "my-profile/your-name.exp".into(),
539 },
540 },
541 );
542
543 let binary_list = BinaryList {
544 rust_build_meta,
545 rust_binaries: vec![fake_bin_test, fake_macro_test],
546 };
547
548 static EXPECTED_HUMAN: &str = indoc! {"
550 fake-package::bin/fake-binary
551 fake-macro::proc-macro/fake-macro
552 "};
553 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {r"
554 fake-package::bin/fake-binary:
555 bin: /fake/binary
556 build platform: target
557 fake-macro::proc-macro/fake-macro:
558 bin: /fake/macro
559 build platform: host
560 "};
561 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
562 {
563 "rust-build-meta": {
564 "target-directory": "/fake/target",
565 "base-output-directories": [
566 "my-profile"
567 ],
568 "non-test-binaries": {
569 "my-package-id": [
570 {
571 "name": "my-name",
572 "kind": "bin-exe",
573 "path": "my-profile/my-name"
574 },
575 {
576 "name": "your-name",
577 "kind": "dylib",
578 "path": "my-profile/your-name.dll"
579 },
580 {
581 "name": "your-name",
582 "kind": "dylib",
583 "path": "my-profile/your-name.exp"
584 }
585 ]
586 },
587 "build-script-out-dirs": {},
588 "linked-paths": [],
589 "platforms": {
590 "host": {
591 "platform": {
592 "triple": "x86_64-unknown-linux-gnu",
593 "target-features": "unknown"
594 },
595 "libdir": {
596 "status": "available",
597 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
598 }
599 },
600 "targets": [
601 {
602 "platform": {
603 "triple": "aarch64-unknown-linux-gnu",
604 "target-features": "unknown"
605 },
606 "libdir": {
607 "status": "unavailable",
608 "reason": "rustc-output-error"
609 }
610 }
611 ]
612 },
613 "target-platforms": [
614 {
615 "triple": "aarch64-unknown-linux-gnu",
616 "target-features": "unknown"
617 }
618 ],
619 "target-platform": "aarch64-unknown-linux-gnu"
620 },
621 "rust-binaries": {
622 "fake-macro::proc-macro/fake-macro": {
623 "binary-id": "fake-macro::proc-macro/fake-macro",
624 "binary-name": "fake-macro",
625 "package-id": "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)",
626 "kind": "proc-macro",
627 "binary-path": "/fake/macro",
628 "build-platform": "host"
629 },
630 "fake-package::bin/fake-binary": {
631 "binary-id": "fake-package::bin/fake-binary",
632 "binary-name": "fake-binary",
633 "package-id": "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)",
634 "kind": "lib",
635 "binary-path": "/fake/binary",
636 "build-platform": "target"
637 }
638 }
639 }"#};
640 static EXPECTED_ONELINE: &str = indoc! {"
642 fake-package::bin/fake-binary
643 fake-macro::proc-macro/fake-macro
644 "};
645 static EXPECTED_ONELINE_VERBOSE: &str = indoc! {r"
646 fake-package::bin/fake-binary [bin: /fake/binary] [build platform: target]
647 fake-macro::proc-macro/fake-macro [bin: /fake/macro] [build platform: host]
648 "};
649
650 assert_eq!(
651 binary_list
652 .to_string(OutputFormat::Human { verbose: false })
653 .expect("human succeeded"),
654 EXPECTED_HUMAN
655 );
656 assert_eq!(
657 binary_list
658 .to_string(OutputFormat::Human { verbose: true })
659 .expect("human succeeded"),
660 EXPECTED_HUMAN_VERBOSE
661 );
662 assert_eq!(
663 binary_list
664 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
665 .expect("json-pretty succeeded"),
666 EXPECTED_JSON_PRETTY
667 );
668 assert_eq!(
669 binary_list
670 .to_string(OutputFormat::Oneline { verbose: false })
671 .expect("oneline succeeded"),
672 EXPECTED_ONELINE
673 );
674 assert_eq!(
675 binary_list
676 .to_string(OutputFormat::Oneline { verbose: true })
677 .expect("oneline verbose succeeded"),
678 EXPECTED_ONELINE_VERBOSE
679 );
680 }
681}