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::{collections::HashSet, io};
21use tracing::warn;
22
23#[derive(Clone, Debug)]
25pub struct RustTestBinary {
26 pub id: RustBinaryId,
28 pub path: Utf8PathBuf,
30 pub package_id: String,
32 pub kind: RustTestBinaryKind,
34 pub name: String,
36 pub build_platform: BuildPlatform,
39}
40
41#[derive(Clone, Debug)]
43pub struct BinaryList {
44 pub rust_build_meta: RustBuildMeta<BinaryListState>,
46
47 pub rust_binaries: Vec<RustTestBinary>,
49}
50
51impl BinaryList {
52 pub fn from_messages(
54 reader: impl io::BufRead,
55 graph: &PackageGraph,
56 build_platforms: BuildPlatforms,
57 ) -> Result<Self, FromMessagesError> {
58 let mut builder = BinaryListBuilder::new(graph, build_platforms);
59
60 for message in Message::parse_stream(reader) {
61 let message = message.map_err(FromMessagesError::ReadMessages)?;
62 builder.process_message(message)?;
63 }
64
65 Ok(builder.finish())
66 }
67
68 pub fn from_summary(summary: BinaryListSummary) -> Result<Self, RustBuildMetaParseError> {
70 let rust_binaries = summary
71 .rust_binaries
72 .into_values()
73 .map(|bin| RustTestBinary {
74 name: bin.binary_name,
75 path: bin.binary_path,
76 package_id: bin.package_id,
77 kind: bin.kind,
78 id: bin.binary_id,
79 build_platform: bin.build_platform,
80 })
81 .collect();
82 Ok(Self {
83 rust_build_meta: RustBuildMeta::from_summary(summary.rust_build_meta)?,
84 rust_binaries,
85 })
86 }
87
88 pub fn write(
90 &self,
91 output_format: OutputFormat,
92 writer: &mut dyn WriteStr,
93 colorize: bool,
94 ) -> Result<(), WriteTestListError> {
95 match output_format {
96 OutputFormat::Human { verbose } => self
97 .write_human(writer, verbose, colorize)
98 .map_err(WriteTestListError::Io),
99 OutputFormat::Oneline { verbose } => self
100 .write_oneline(writer, verbose, colorize)
101 .map_err(WriteTestListError::Io),
102 OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
103 }
104 }
105
106 fn to_summary(&self) -> BinaryListSummary {
107 let rust_binaries = self
108 .rust_binaries
109 .iter()
110 .map(|bin| {
111 let summary = RustTestBinarySummary {
112 binary_name: bin.name.clone(),
113 package_id: bin.package_id.clone(),
114 kind: bin.kind.clone(),
115 binary_path: bin.path.clone(),
116 binary_id: bin.id.clone(),
117 build_platform: bin.build_platform,
118 };
119 (bin.id.clone(), summary)
120 })
121 .collect();
122
123 BinaryListSummary {
124 rust_build_meta: self.rust_build_meta.to_summary(),
125 rust_binaries,
126 }
127 }
128
129 fn write_human(
130 &self,
131 writer: &mut dyn WriteStr,
132 verbose: bool,
133 colorize: bool,
134 ) -> io::Result<()> {
135 let mut styles = Styles::default();
136 if colorize {
137 styles.colorize();
138 }
139 for bin in &self.rust_binaries {
140 if verbose {
141 writeln!(writer, "{}:", bin.id.style(styles.binary_id))?;
142 writeln!(writer, " {} {}", "bin:".style(styles.field), bin.path)?;
143 writeln!(
144 writer,
145 " {} {}",
146 "build platform:".style(styles.field),
147 bin.build_platform,
148 )?;
149 } else {
150 writeln!(writer, "{}", bin.id.style(styles.binary_id))?;
151 }
152 }
153 Ok(())
154 }
155
156 fn write_oneline(
157 &self,
158 writer: &mut dyn WriteStr,
159 verbose: bool,
160 colorize: bool,
161 ) -> io::Result<()> {
162 let mut styles = Styles::default();
163 if colorize {
164 styles.colorize();
165 }
166 for bin in &self.rust_binaries {
167 write!(writer, "{}", bin.id.style(styles.binary_id))?;
168 if verbose {
169 write!(
170 writer,
171 " [{}{}] [{}{}]",
172 "bin: ".style(styles.field),
173 bin.path,
174 "build platform: ".style(styles.field),
175 bin.build_platform,
176 )?;
177 }
178 writeln!(writer)?;
179 }
180 Ok(())
181 }
182
183 pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
185 let mut s = String::with_capacity(1024);
186 self.write(output_format, &mut s, false)?;
187 Ok(s)
188 }
189}
190
191#[derive(Debug)]
193pub struct BinaryListBuilder<'g> {
194 state: BinaryListBuildState<'g>,
195}
196
197impl<'g> BinaryListBuilder<'g> {
198 pub fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
200 Self {
201 state: BinaryListBuildState::new(graph, build_platforms),
202 }
203 }
204
205 pub fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
207 self.state.process_message(message)
208 }
209
210 pub fn process_message_line(&mut self, line: &str) -> Result<(), FromMessagesError> {
215 self.process_message(parse_message_line(line))
216 }
217
218 pub fn finish(self) -> BinaryList {
220 self.state.finish()
221 }
222}
223
224fn parse_message_line(line: &str) -> Message {
226 let mut deserializer = serde_json::Deserializer::from_str(line);
227 deserializer.disable_recursion_limit();
228 Message::deserialize(&mut deserializer).unwrap_or_else(|_| Message::TextLine(line.to_owned()))
229}
230
231#[derive(Debug)]
232struct BinaryListBuildState<'g> {
233 graph: &'g PackageGraph,
234 rust_binaries: Vec<RustTestBinary>,
235 rust_build_meta: RustBuildMeta<BinaryListState>,
236 alt_target_dir: Option<Utf8PathBuf>,
237}
238
239impl<'g> BinaryListBuildState<'g> {
240 fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
241 let rust_target_dir = graph.workspace().target_directory().to_path_buf();
242 let alt_target_dir = std::env::var("__NEXTEST_ALT_TARGET_DIR")
244 .ok()
245 .map(Utf8PathBuf::from);
246
247 Self {
248 graph,
249 rust_binaries: vec![],
250 rust_build_meta: RustBuildMeta::new(rust_target_dir, build_platforms),
251 alt_target_dir,
252 }
253 }
254
255 fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
256 match message {
257 Message::CompilerArtifact(artifact) => {
258 self.process_artifact(artifact)?;
259 }
260 Message::BuildScriptExecuted(build_script) => {
261 self.process_build_script(build_script)?;
262 }
263 _ => {
264 }
266 }
267
268 Ok(())
269 }
270
271 fn process_artifact(&mut self, artifact: Artifact) -> Result<(), FromMessagesError> {
272 if let Some(path) = artifact.executable {
273 self.detect_base_output_dir(&path);
274
275 if artifact.profile.test {
276 let package_id = artifact.package_id.repr;
277
278 let name = artifact.target.name;
281
282 let package = self
283 .graph
284 .metadata(&guppy::PackageId::new(package_id.clone()))
285 .map_err(FromMessagesError::PackageGraph)?;
286
287 let kind = artifact.target.kind;
288 if kind.is_empty() {
289 return Err(FromMessagesError::MissingTargetKind {
290 package_name: package.name().to_owned(),
291 binary_name: name.clone(),
292 });
293 }
294
295 let (computed_kind, platform) = if kind.iter().any(|k| {
296 matches!(
298 k,
299 TargetKind::Lib
300 | TargetKind::RLib
301 | TargetKind::DyLib
302 | TargetKind::CDyLib
303 | TargetKind::StaticLib
304 )
305 }) {
306 (RustTestBinaryKind::LIB, BuildPlatform::Target)
307 } else if let Some(TargetKind::ProcMacro) = kind.first() {
308 (RustTestBinaryKind::PROC_MACRO, BuildPlatform::Host)
309 } else {
310 (
312 RustTestBinaryKind::new(
313 kind.into_iter()
314 .next()
315 .expect("already checked that kind is non-empty")
316 .to_string(),
317 ),
318 BuildPlatform::Target,
319 )
320 };
321
322 let id = RustBinaryId::from_parts(package.name(), &computed_kind, &name);
324
325 self.rust_binaries.push(RustTestBinary {
326 path,
327 package_id,
328 kind: computed_kind,
329 name,
330 id,
331 build_platform: platform,
332 });
333 } else if artifact
334 .target
335 .kind
336 .iter()
337 .any(|x| matches!(x, TargetKind::Bin))
338 {
339 if let Ok(rel_path) = path.strip_prefix(&self.rust_build_meta.target_directory) {
343 let non_test_binary = RustNonTestBinarySummary {
344 name: artifact.target.name,
345 kind: RustNonTestBinaryKind::BIN_EXE,
346 path: convert_rel_path_to_forward_slash(rel_path),
347 };
348
349 self.rust_build_meta
350 .non_test_binaries
351 .entry(artifact.package_id.repr)
352 .or_default()
353 .insert(non_test_binary);
354 };
355 }
356 } else if artifact
357 .target
358 .kind
359 .iter()
360 .any(|x| matches!(x, TargetKind::DyLib | TargetKind::CDyLib))
361 {
362 for filename in artifact.filenames {
364 if let Ok(rel_path) = filename.strip_prefix(&self.rust_build_meta.target_directory)
365 {
366 let non_test_binary = RustNonTestBinarySummary {
367 name: artifact.target.name.clone(),
368 kind: RustNonTestBinaryKind::DYLIB,
369 path: convert_rel_path_to_forward_slash(rel_path),
370 };
371 self.rust_build_meta
372 .non_test_binaries
373 .entry(artifact.package_id.repr.clone())
374 .or_default()
375 .insert(non_test_binary);
376 }
377 }
378 }
379
380 Ok(())
381 }
382
383 fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
395 let rel_path = artifact_path
397 .strip_prefix(&self.rust_build_meta.target_directory)
398 .ok()?;
399 let parent = rel_path.parent()?;
400 if parent.file_name() == Some("deps") {
401 let base = parent.parent()?;
402 if !self.rust_build_meta.base_output_directories.contains(base) {
403 self.rust_build_meta
404 .base_output_directories
405 .insert(convert_rel_path_to_forward_slash(base));
406 }
407 }
408 Some(())
409 }
410
411 fn process_build_script(&mut self, build_script: BuildScript) -> Result<(), FromMessagesError> {
412 for path in build_script.linked_paths {
413 self.detect_linked_path(&build_script.package_id, &path);
414 }
415
416 let package_id = guppy::PackageId::new(build_script.package_id.repr);
418 let in_workspace = self.graph.metadata(&package_id).map_or_else(
419 |_| {
420 warn!(
422 target: "nextest-runner::list",
423 "warning: saw package ID `{}` which wasn't produced by cargo metadata",
424 package_id
425 );
426 false
427 },
428 |p| p.in_workspace(),
429 );
430 if in_workspace {
431 if let Ok(rel_out_dir) = build_script
433 .out_dir
434 .strip_prefix(&self.rust_build_meta.target_directory)
435 {
436 self.rust_build_meta.build_script_out_dirs.insert(
437 package_id.repr().to_owned(),
438 convert_rel_path_to_forward_slash(rel_out_dir),
439 );
440 }
441 }
442
443 Ok(())
444 }
445
446 fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
448 let actual_path = match path.as_str().split_once('=') {
450 Some((_, p)) => p.into(),
451 None => path,
452 };
453
454 let rel_path = match actual_path.strip_prefix(&self.rust_build_meta.target_directory) {
455 Ok(rel) => rel,
456 Err(_) => {
457 if let Some(alt_target_dir) = &self.alt_target_dir {
469 actual_path.strip_prefix(alt_target_dir).ok()?
470 } else {
471 return None;
472 }
473 }
474 };
475
476 self.rust_build_meta
477 .linked_paths
478 .entry(convert_rel_path_to_forward_slash(rel_path))
479 .or_default()
480 .insert(package_id.repr.clone());
481
482 Some(())
483 }
484
485 fn finish(mut self) -> BinaryList {
486 self.rust_binaries.sort_by(|b1, b2| b1.id.cmp(&b2.id));
487
488 let relevant_package_ids = self
490 .rust_binaries
491 .iter()
492 .map(|bin| bin.package_id.clone())
493 .collect::<HashSet<_>>();
494
495 self.rust_build_meta
496 .build_script_out_dirs
497 .retain(|package_id, _| relevant_package_ids.contains(package_id));
498
499 BinaryList {
500 rust_build_meta: self.rust_build_meta,
501 rust_binaries: self.rust_binaries,
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use crate::{
510 cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
511 list::{
512 SerializableFormat,
513 test_helpers::{PACKAGE_GRAPH_FIXTURE, PACKAGE_METADATA_ID, package_metadata},
514 },
515 platform::{HostPlatform, PlatformLibdir, TargetPlatform},
516 };
517 use indoc::indoc;
518 use maplit::btreeset;
519 use nextest_metadata::PlatformLibdirUnavailable;
520 use pretty_assertions::assert_eq;
521 use serde_json::json;
522 use target_spec::{Platform, TargetFeatures};
523
524 #[test]
525 fn test_parse_binary_list() {
526 let fake_bin_test = RustTestBinary {
527 id: "fake-package::bin/fake-binary".into(),
528 path: "/fake/binary".into(),
529 package_id: "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)"
530 .to_owned(),
531 kind: RustTestBinaryKind::LIB,
532 name: "fake-binary".to_owned(),
533 build_platform: BuildPlatform::Target,
534 };
535 let fake_macro_test = RustTestBinary {
536 id: "fake-macro::proc-macro/fake-macro".into(),
537 path: "/fake/macro".into(),
538 package_id: "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)"
539 .to_owned(),
540 kind: RustTestBinaryKind::PROC_MACRO,
541 name: "fake-macro".to_owned(),
542 build_platform: BuildPlatform::Host,
543 };
544
545 let fake_triple = TargetTriple {
546 platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
547 source: TargetTripleSource::CliOption,
548 location: TargetDefinitionLocation::Builtin,
549 };
550 let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
551 let build_platforms = BuildPlatforms {
552 host: HostPlatform {
553 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
554 libdir: PlatformLibdir::Available(Utf8PathBuf::from(fake_host_libdir)),
555 },
556 target: Some(TargetPlatform {
557 triple: fake_triple,
558 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
560 }),
561 };
562
563 let mut rust_build_meta = RustBuildMeta::new("/fake/target", build_platforms);
564 rust_build_meta
565 .base_output_directories
566 .insert("my-profile".into());
567 rust_build_meta.non_test_binaries.insert(
568 "my-package-id".into(),
569 btreeset! {
570 RustNonTestBinarySummary {
571 name: "my-name".into(),
572 kind: RustNonTestBinaryKind::BIN_EXE,
573 path: "my-profile/my-name".into(),
574 },
575 RustNonTestBinarySummary {
576 name: "your-name".into(),
577 kind: RustNonTestBinaryKind::DYLIB,
578 path: "my-profile/your-name.dll".into(),
579 },
580 RustNonTestBinarySummary {
581 name: "your-name".into(),
582 kind: RustNonTestBinaryKind::DYLIB,
583 path: "my-profile/your-name.exp".into(),
584 },
585 },
586 );
587
588 let binary_list = BinaryList {
589 rust_build_meta,
590 rust_binaries: vec![fake_bin_test, fake_macro_test],
591 };
592
593 static EXPECTED_HUMAN: &str = indoc! {"
595 fake-package::bin/fake-binary
596 fake-macro::proc-macro/fake-macro
597 "};
598 static EXPECTED_HUMAN_VERBOSE: &str = indoc! {r"
599 fake-package::bin/fake-binary:
600 bin: /fake/binary
601 build platform: target
602 fake-macro::proc-macro/fake-macro:
603 bin: /fake/macro
604 build platform: host
605 "};
606 static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
607 {
608 "rust-build-meta": {
609 "target-directory": "/fake/target",
610 "base-output-directories": [
611 "my-profile"
612 ],
613 "non-test-binaries": {
614 "my-package-id": [
615 {
616 "name": "my-name",
617 "kind": "bin-exe",
618 "path": "my-profile/my-name"
619 },
620 {
621 "name": "your-name",
622 "kind": "dylib",
623 "path": "my-profile/your-name.dll"
624 },
625 {
626 "name": "your-name",
627 "kind": "dylib",
628 "path": "my-profile/your-name.exp"
629 }
630 ]
631 },
632 "build-script-out-dirs": {},
633 "linked-paths": [],
634 "platforms": {
635 "host": {
636 "platform": {
637 "triple": "x86_64-unknown-linux-gnu",
638 "target-features": "unknown"
639 },
640 "libdir": {
641 "status": "available",
642 "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
643 }
644 },
645 "targets": [
646 {
647 "platform": {
648 "triple": "aarch64-unknown-linux-gnu",
649 "target-features": "unknown"
650 },
651 "libdir": {
652 "status": "unavailable",
653 "reason": "rustc-output-error"
654 }
655 }
656 ]
657 },
658 "target-platforms": [
659 {
660 "triple": "aarch64-unknown-linux-gnu",
661 "target-features": "unknown"
662 }
663 ],
664 "target-platform": "aarch64-unknown-linux-gnu"
665 },
666 "rust-binaries": {
667 "fake-macro::proc-macro/fake-macro": {
668 "binary-id": "fake-macro::proc-macro/fake-macro",
669 "binary-name": "fake-macro",
670 "package-id": "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)",
671 "kind": "proc-macro",
672 "binary-path": "/fake/macro",
673 "build-platform": "host"
674 },
675 "fake-package::bin/fake-binary": {
676 "binary-id": "fake-package::bin/fake-binary",
677 "binary-name": "fake-binary",
678 "package-id": "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)",
679 "kind": "lib",
680 "binary-path": "/fake/binary",
681 "build-platform": "target"
682 }
683 }
684 }"#};
685 static EXPECTED_ONELINE: &str = indoc! {"
687 fake-package::bin/fake-binary
688 fake-macro::proc-macro/fake-macro
689 "};
690 static EXPECTED_ONELINE_VERBOSE: &str = indoc! {r"
691 fake-package::bin/fake-binary [bin: /fake/binary] [build platform: target]
692 fake-macro::proc-macro/fake-macro [bin: /fake/macro] [build platform: host]
693 "};
694
695 assert_eq!(
696 binary_list
697 .to_string(OutputFormat::Human { verbose: false })
698 .expect("human succeeded"),
699 EXPECTED_HUMAN
700 );
701 assert_eq!(
702 binary_list
703 .to_string(OutputFormat::Human { verbose: true })
704 .expect("human succeeded"),
705 EXPECTED_HUMAN_VERBOSE
706 );
707 assert_eq!(
708 binary_list
709 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
710 .expect("json-pretty succeeded"),
711 EXPECTED_JSON_PRETTY
712 );
713 assert_eq!(
714 binary_list
715 .to_string(OutputFormat::Oneline { verbose: false })
716 .expect("oneline succeeded"),
717 EXPECTED_ONELINE
718 );
719 assert_eq!(
720 binary_list
721 .to_string(OutputFormat::Oneline { verbose: true })
722 .expect("oneline verbose succeeded"),
723 EXPECTED_ONELINE_VERBOSE
724 );
725 }
726
727 #[test]
728 fn test_parse_binary_list_from_message_lines() {
729 let build_platforms = BuildPlatforms {
730 host: HostPlatform {
731 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
732 libdir: PlatformLibdir::Available("/fake/libdir".into()),
733 },
734 target: None,
735 };
736 let package = package_metadata();
737 let artifact_path = PACKAGE_GRAPH_FIXTURE
738 .workspace()
739 .target_directory()
740 .join("debug/deps/metadata_helper-test");
741 let src_path = package
742 .manifest_path()
743 .parent()
744 .expect("manifest path has a parent")
745 .join("src/lib.rs");
746
747 let compiler_artifact = json!({
748 "reason": "compiler-artifact",
749 "package_id": PACKAGE_METADATA_ID,
750 "manifest_path": package.manifest_path(),
751 "target": {
752 "name": package.name(),
753 "kind": ["lib"],
754 "crate_types": ["lib"],
755 "required-features": [],
756 "src_path": src_path,
757 "edition": "2021",
758 "doctest": true,
759 "test": true,
760 "doc": true
761 },
762 "profile": {
763 "opt_level": "0",
764 "debuginfo": 0,
765 "debug_assertions": true,
766 "overflow_checks": true,
767 "test": true
768 },
769 "features": [],
770 "filenames": [artifact_path],
771 "executable": artifact_path,
772 "fresh": false
773 });
774 let input = format!("this is not JSON\n{}\n\n", compiler_artifact);
775
776 let from_messages = BinaryList::from_messages(
777 input.as_bytes(),
778 &PACKAGE_GRAPH_FIXTURE,
779 build_platforms.clone(),
780 )
781 .expect("parsing from messages succeeds");
782
783 let mut builder = BinaryListBuilder::new(&PACKAGE_GRAPH_FIXTURE, build_platforms);
784 for line in input.lines() {
785 builder
786 .process_message_line(line)
787 .expect("processing line succeeds");
788 }
789 let from_lines = builder.finish();
790
791 assert_eq!(
792 from_lines
793 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
794 .expect("json-pretty succeeds"),
795 from_messages
796 .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
797 .expect("json-pretty succeeds")
798 );
799 }
800}