nextest_runner/list/
binary_list.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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/// A Rust test binary built by Cargo.
27#[derive(Clone, Debug)]
28pub struct RustTestBinary {
29    /// A unique ID.
30    pub id: RustBinaryId,
31    /// The path to the binary artifact.
32    pub path: Utf8PathBuf,
33    /// The package this artifact belongs to.
34    pub package_id: String,
35    /// The kind of Rust test binary this is.
36    pub kind: RustTestBinaryKind,
37    /// The unique binary name defined in `Cargo.toml` or inferred by the filename.
38    pub name: String,
39    /// Platform for which this binary was built.
40    /// (Proc-macro tests are built for the host.)
41    pub build_platform: BuildPlatform,
42}
43
44/// The list of Rust test binaries built by Cargo.
45#[derive(Clone, Debug)]
46pub struct BinaryList {
47    /// Rust-related metadata.
48    pub rust_build_meta: RustBuildMeta<BinaryListState>,
49
50    /// The list of test binaries.
51    pub rust_binaries: Vec<RustTestBinary>,
52}
53
54impl BinaryList {
55    /// Parses Cargo messages from the given `BufRead` and returns a list of test binaries.
56    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    /// Constructs the list from its summary format
72    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    /// Outputs this list to the given writer.
92    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    /// Produces a summary suitable for archive metadata.
117    ///
118    /// * `build_directory` is omitted so it defaults to `target_directory` on
119    ///   extraction.
120    /// * Binary paths under `build_directory` are remapped to `target_directory`
121    ///   so the `PathMapper` can remap them correctly on extraction.
122    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                // In the archive, test binaries are stored under target/.
131                // Remap paths from build_directory to target_directory so the PathMapper
132                // can relocate them on extraction.
133                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    /// Outputs this list as a string with the given format.
228    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/// Incrementally builds a [`BinaryList`] from Cargo messages.
236#[derive(Debug)]
237pub struct BinaryListBuilder<'g> {
238    state: BinaryListBuildState<'g>,
239}
240
241impl<'g> BinaryListBuilder<'g> {
242    /// Creates a new builder for Cargo messages.
243    pub fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
244        Self {
245            state: BinaryListBuildState::new(graph, build_platforms),
246        }
247    }
248
249    /// Processes a single Cargo message.
250    pub fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
251        self.state.process_message(message)
252    }
253
254    /// Processes a single line of Cargo output.
255    ///
256    /// This uses the same single-line parsing behavior as
257    /// [`cargo_metadata::Message::parse_stream`].
258    pub fn process_message_line(&mut self, line: &str) -> Result<(), FromMessagesError> {
259        self.process_message(parse_message_line(line))
260    }
261
262    /// Finishes building the binary list.
263    pub fn finish(self) -> BinaryList {
264        self.state.finish()
265    }
266}
267
268// Adapted from cargo_metadata::MessageIter::next (cargo_metadata 0.23.1).
269fn 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        // Use the build directory if on Cargo 1.91 or newer. Fall back to
287        // the target directory for older Cargo versions.
288        let build_directory = graph
289            .workspace()
290            .build_directory()
291            .unwrap_or_else(|| graph.workspace().target_directory())
292            .to_path_buf();
293        // For testing only, not part of the public API.
294        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                // Ignore all other messages.
316            }
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                // Look up the executable by package ID.
330
331                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                    // https://doc.rust-lang.org/nightly/cargo/reference/cargo-targets.html#the-crate-type-field
348                    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                    // Non-lib kinds should always have just one element. Grab the first one.
362                    (
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                // Construct the binary ID from the package and build target.
374                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                // This is a non-test binary -- add it to the map.
391                // Error case here implies that the returned path wasn't in the target directory -- ignore it
392                // since it shouldn't happen in normal use.
393                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                    };
399
400                    self.rust_build_meta
401                        .non_test_binaries
402                        .entry(artifact.package_id.repr)
403                        .or_default()
404                        .insert(non_test_binary);
405                };
406            }
407        } else if artifact
408            .target
409            .kind
410            .iter()
411            .any(|x| matches!(x, TargetKind::DyLib | TargetKind::CDyLib))
412        {
413            // Also look for and grab dynamic libraries to store in archives.
414            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                    };
422                    self.rust_build_meta
423                        .non_test_binaries
424                        .entry(artifact.package_id.repr.clone())
425                        .or_default()
426                        .insert(non_test_binary);
427                }
428            }
429        }
430
431        Ok(())
432    }
433
434    /// Look for paths that contain "deps" in their second-to-last component,
435    /// and are descendants of the target directory.
436    /// The paths without "deps" are base output directories.
437    ///
438    /// e.g. path/to/repo/target/debug/deps/test-binary => add "debug"
439    /// to base output dirs.
440    ///
441    /// Note that test binaries are always present in "deps", so we should always
442    /// have a match.
443    ///
444    /// The `Option` in the return value is to let ? work.
445    fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
446        // Artifact paths must be relative to the build directory (which
447        // equals the target directory unless Cargo's build.build-dir is
448        // configured).
449        let rel_path = match artifact_path.strip_prefix(&self.rust_build_meta.build_directory) {
450            Ok(rel) => rel,
451            Err(_) => {
452                debug!(
453                    target: "nextest-runner::list",
454                    "artifact path `{}` is not within the build directory `{}`, \
455                     skipping base output directory detection",
456                    artifact_path, self.rust_build_meta.build_directory,
457                );
458                return None;
459            }
460        };
461        let parent = rel_path.parent()?;
462        if parent.file_name() == Some("deps") {
463            let base = parent.parent()?;
464            if !self.rust_build_meta.base_output_directories.contains(base) {
465                self.rust_build_meta
466                    .base_output_directories
467                    .insert(convert_rel_path_to_forward_slash(base));
468            }
469        }
470        Some(())
471    }
472
473    fn process_build_script(&mut self, build_script: BuildScript) -> Result<(), FromMessagesError> {
474        for path in build_script.linked_paths {
475            self.detect_linked_path(&build_script.package_id, &path);
476        }
477
478        // We only care about build scripts for workspace packages.
479        let package_id = guppy::PackageId::new(build_script.package_id.repr);
480        let in_workspace = self.graph.metadata(&package_id).map_or_else(
481            |_| {
482                // Warn about processing a package that isn't in the package graph.
483                warn!(
484                    target: "nextest-runner::list",
485                    "warning: saw package ID `{}` which wasn't produced by cargo metadata",
486                    package_id
487                );
488                false
489            },
490            |p| p.in_workspace(),
491        );
492        if in_workspace {
493            // Build script out_dirs are relative to the build directory.
494            match build_script
495                .out_dir
496                .strip_prefix(&self.rust_build_meta.build_directory)
497            {
498                Ok(rel_out_dir) => {
499                    self.rust_build_meta.build_script_out_dirs.insert(
500                        package_id.repr().to_owned(),
501                        convert_rel_path_to_forward_slash(rel_out_dir),
502                    );
503                }
504                Err(_) => {
505                    debug!(
506                        target: "nextest-runner::list",
507                        "build script out_dir `{}` for package `{}` is not within \
508                         the build directory `{}`, skipping",
509                        build_script.out_dir, package_id,
510                        self.rust_build_meta.build_directory,
511                    );
512                }
513            }
514
515            // Capture build script environment variables from the structured
516            // cargo message, avoiding the need to parse the raw output file.
517            if !build_script.env.is_empty() {
518                self.rust_build_meta
519                    .build_script_info
520                    .get_or_insert_with(BTreeMap::new)
521                    .entry(package_id.repr().to_owned())
522                    .or_default()
523                    .envs = build_script.env.into_iter().collect();
524            }
525        }
526
527        Ok(())
528    }
529
530    /// The `Option` in the return value is to let ? work.
531    fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
532        // Remove anything up to the first "=" (e.g. "native=").
533        let actual_path = match path.as_str().split_once('=') {
534            Some((_, p)) => p.into(),
535            None => path,
536        };
537
538        let rel_path = match actual_path.strip_prefix(&self.rust_build_meta.build_directory) {
539            Ok(rel) => rel,
540            Err(_) => {
541                // For a seeded build (like in our test suite), Cargo will
542                // return:
543                //
544                // * the new path if the linked path exists
545                // * the original path if the linked path does not exist
546                //
547                // Linked paths not existing is not an ordinary condition, but
548                // we want to test it within nextest. We filter out paths if
549                // they're not a subdirectory of the target directory. With
550                // __NEXTEST_ALT_TARGET_DIR, we can simulate that for an
551                // alternate target directory.
552                if let Some(alt_target_dir) = &self.alt_target_dir {
553                    actual_path.strip_prefix(alt_target_dir).ok()?
554                } else {
555                    return None;
556                }
557            }
558        };
559
560        self.rust_build_meta
561            .linked_paths
562            .entry(convert_rel_path_to_forward_slash(rel_path))
563            .or_default()
564            .insert(package_id.repr.clone());
565
566        Some(())
567    }
568
569    fn finish(mut self) -> BinaryList {
570        self.rust_binaries.sort_by(|b1, b2| b1.id.cmp(&b2.id));
571
572        // Clean out any build script output directories for which there's no corresponding binary.
573        let relevant_package_ids = self
574            .rust_binaries
575            .iter()
576            .map(|bin| bin.package_id.clone())
577            .collect::<HashSet<_>>();
578
579        self.rust_build_meta
580            .build_script_out_dirs
581            .retain(|package_id, _| relevant_package_ids.contains(package_id));
582        if let Some(info) = &mut self.rust_build_meta.build_script_info {
583            info.retain(|package_id, _| relevant_package_ids.contains(package_id));
584        }
585
586        BinaryList {
587            rust_build_meta: self.rust_build_meta,
588            rust_binaries: self.rust_binaries,
589        }
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use crate::{
597        cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
598        list::{
599            SerializableFormat,
600            test_helpers::{PACKAGE_GRAPH_FIXTURE, PACKAGE_METADATA_ID, package_metadata},
601        },
602        platform::{HostPlatform, PlatformLibdir, TargetPlatform},
603    };
604    use indoc::indoc;
605    use maplit::btreeset;
606    use nextest_metadata::PlatformLibdirUnavailable;
607    use pretty_assertions::assert_eq;
608    use serde_json::json;
609    use target_spec::{Platform, TargetFeatures};
610
611    #[test]
612    fn test_parse_binary_list() {
613        let fake_bin_test = RustTestBinary {
614            id: "fake-package::bin/fake-binary".into(),
615            path: "/fake/binary".into(),
616            package_id: "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)"
617                .to_owned(),
618            kind: RustTestBinaryKind::LIB,
619            name: "fake-binary".to_owned(),
620            build_platform: BuildPlatform::Target,
621        };
622        let fake_macro_test = RustTestBinary {
623            id: "fake-macro::proc-macro/fake-macro".into(),
624            path: "/fake/macro".into(),
625            package_id: "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)"
626                .to_owned(),
627            kind: RustTestBinaryKind::PROC_MACRO,
628            name: "fake-macro".to_owned(),
629            build_platform: BuildPlatform::Host,
630        };
631
632        let fake_triple = TargetTriple {
633            platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
634            source: TargetTripleSource::CliOption,
635            location: TargetDefinitionLocation::Builtin,
636        };
637        let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
638        let build_platforms = BuildPlatforms {
639            host: HostPlatform {
640                platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
641                libdir: PlatformLibdir::Available(Utf8PathBuf::from(fake_host_libdir)),
642            },
643            target: Some(TargetPlatform {
644                triple: fake_triple,
645                // Test out the error case for unavailable libdirs.
646                libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
647            }),
648        };
649
650        let mut rust_build_meta =
651            RustBuildMeta::new("/fake/target", "/fake/target", build_platforms);
652        rust_build_meta
653            .base_output_directories
654            .insert("my-profile".into());
655        rust_build_meta.non_test_binaries.insert(
656            "my-package-id".into(),
657            btreeset! {
658                RustNonTestBinarySummary {
659                    name: "my-name".into(),
660                    kind: RustNonTestBinaryKind::BIN_EXE,
661                    path: "my-profile/my-name".into(),
662                },
663                RustNonTestBinarySummary {
664                    name: "your-name".into(),
665                    kind: RustNonTestBinaryKind::DYLIB,
666                    path: "my-profile/your-name.dll".into(),
667                },
668                RustNonTestBinarySummary {
669                    name: "your-name".into(),
670                    kind: RustNonTestBinaryKind::DYLIB,
671                    path: "my-profile/your-name.exp".into(),
672                },
673            },
674        );
675
676        let binary_list = BinaryList {
677            rust_build_meta,
678            rust_binaries: vec![fake_bin_test, fake_macro_test],
679        };
680
681        // Check that the expected outputs are valid.
682        static EXPECTED_HUMAN: &str = indoc! {"
683        fake-package::bin/fake-binary
684        fake-macro::proc-macro/fake-macro
685        "};
686        static EXPECTED_HUMAN_VERBOSE: &str = indoc! {r"
687        fake-package::bin/fake-binary:
688          bin: /fake/binary
689          build platform: target
690        fake-macro::proc-macro/fake-macro:
691          bin: /fake/macro
692          build platform: host
693        "};
694        static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
695        {
696          "rust-build-meta": {
697            "target-directory": "/fake/target",
698            "build-directory": "/fake/target",
699            "base-output-directories": [
700              "my-profile"
701            ],
702            "non-test-binaries": {
703              "my-package-id": [
704                {
705                  "name": "my-name",
706                  "kind": "bin-exe",
707                  "path": "my-profile/my-name"
708                },
709                {
710                  "name": "your-name",
711                  "kind": "dylib",
712                  "path": "my-profile/your-name.dll"
713                },
714                {
715                  "name": "your-name",
716                  "kind": "dylib",
717                  "path": "my-profile/your-name.exp"
718                }
719              ]
720            },
721            "build-script-out-dirs": {},
722            "build-script-info": {},
723            "linked-paths": [],
724            "platforms": {
725              "host": {
726                "platform": {
727                  "triple": "x86_64-unknown-linux-gnu",
728                  "target-features": "unknown"
729                },
730                "libdir": {
731                  "status": "available",
732                  "path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
733                }
734              },
735              "targets": [
736                {
737                  "platform": {
738                    "triple": "aarch64-unknown-linux-gnu",
739                    "target-features": "unknown"
740                  },
741                  "libdir": {
742                    "status": "unavailable",
743                    "reason": "rustc-output-error"
744                  }
745                }
746              ]
747            },
748            "target-platforms": [
749              {
750                "triple": "aarch64-unknown-linux-gnu",
751                "target-features": "unknown"
752              }
753            ],
754            "target-platform": "aarch64-unknown-linux-gnu"
755          },
756          "rust-binaries": {
757            "fake-macro::proc-macro/fake-macro": {
758              "binary-id": "fake-macro::proc-macro/fake-macro",
759              "binary-name": "fake-macro",
760              "package-id": "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)",
761              "kind": "proc-macro",
762              "binary-path": "/fake/macro",
763              "build-platform": "host"
764            },
765            "fake-package::bin/fake-binary": {
766              "binary-id": "fake-package::bin/fake-binary",
767              "binary-name": "fake-binary",
768              "package-id": "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)",
769              "kind": "lib",
770              "binary-path": "/fake/binary",
771              "build-platform": "target"
772            }
773          }
774        }"#};
775        // Non-verbose oneline is the same as non-verbose human.
776        static EXPECTED_ONELINE: &str = indoc! {"
777            fake-package::bin/fake-binary
778            fake-macro::proc-macro/fake-macro
779        "};
780        static EXPECTED_ONELINE_VERBOSE: &str = indoc! {r"
781            fake-package::bin/fake-binary [bin: /fake/binary] [build platform: target]
782            fake-macro::proc-macro/fake-macro [bin: /fake/macro] [build platform: host]
783        "};
784
785        assert_eq!(
786            binary_list
787                .to_string(OutputFormat::Human { verbose: false })
788                .expect("human succeeded"),
789            EXPECTED_HUMAN
790        );
791        assert_eq!(
792            binary_list
793                .to_string(OutputFormat::Human { verbose: true })
794                .expect("human succeeded"),
795            EXPECTED_HUMAN_VERBOSE
796        );
797        assert_eq!(
798            binary_list
799                .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
800                .expect("json-pretty succeeded"),
801            EXPECTED_JSON_PRETTY
802        );
803        assert_eq!(
804            binary_list
805                .to_string(OutputFormat::Oneline { verbose: false })
806                .expect("oneline succeeded"),
807            EXPECTED_ONELINE
808        );
809        assert_eq!(
810            binary_list
811                .to_string(OutputFormat::Oneline { verbose: true })
812                .expect("oneline verbose succeeded"),
813            EXPECTED_ONELINE_VERBOSE
814        );
815    }
816
817    #[test]
818    fn test_parse_binary_list_from_message_lines() {
819        let build_platforms = BuildPlatforms {
820            host: HostPlatform {
821                platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
822                libdir: PlatformLibdir::Available("/fake/libdir".into()),
823            },
824            target: None,
825        };
826        let package = package_metadata();
827        let artifact_path = PACKAGE_GRAPH_FIXTURE
828            .workspace()
829            .target_directory()
830            .join("debug/deps/metadata_helper-test");
831        let src_path = package
832            .manifest_path()
833            .parent()
834            .expect("manifest path has a parent")
835            .join("src/lib.rs");
836
837        let compiler_artifact = json!({
838            "reason": "compiler-artifact",
839            "package_id": PACKAGE_METADATA_ID,
840            "manifest_path": package.manifest_path(),
841            "target": {
842                "name": package.name(),
843                "kind": ["lib"],
844                "crate_types": ["lib"],
845                "required-features": [],
846                "src_path": src_path,
847                "edition": "2021",
848                "doctest": true,
849                "test": true,
850                "doc": true
851            },
852            "profile": {
853                "opt_level": "0",
854                "debuginfo": 0,
855                "debug_assertions": true,
856                "overflow_checks": true,
857                "test": true
858            },
859            "features": [],
860            "filenames": [artifact_path],
861            "executable": artifact_path,
862            "fresh": false
863        });
864        let input = format!("this is not JSON\n{}\n\n", compiler_artifact);
865
866        let from_messages = BinaryList::from_messages(
867            input.as_bytes(),
868            &PACKAGE_GRAPH_FIXTURE,
869            build_platforms.clone(),
870        )
871        .expect("parsing from messages succeeds");
872
873        let mut builder = BinaryListBuilder::new(&PACKAGE_GRAPH_FIXTURE, build_platforms);
874        for line in input.lines() {
875            builder
876                .process_message_line(line)
877                .expect("processing line succeeds");
878        }
879        let from_lines = builder.finish();
880
881        assert_eq!(
882            from_lines
883                .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
884                .expect("json-pretty succeeds"),
885            from_messages
886                .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
887                .expect("json-pretty succeeds")
888        );
889    }
890}