Skip to main content

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::{collections::HashSet, io};
21use tracing::warn;
22
23/// A Rust test binary built by Cargo.
24#[derive(Clone, Debug)]
25pub struct RustTestBinary {
26    /// A unique ID.
27    pub id: RustBinaryId,
28    /// The path to the binary artifact.
29    pub path: Utf8PathBuf,
30    /// The package this artifact belongs to.
31    pub package_id: String,
32    /// The kind of Rust test binary this is.
33    pub kind: RustTestBinaryKind,
34    /// The unique binary name defined in `Cargo.toml` or inferred by the filename.
35    pub name: String,
36    /// Platform for which this binary was built.
37    /// (Proc-macro tests are built for the host.)
38    pub build_platform: BuildPlatform,
39}
40
41/// The list of Rust test binaries built by Cargo.
42#[derive(Clone, Debug)]
43pub struct BinaryList {
44    /// Rust-related metadata.
45    pub rust_build_meta: RustBuildMeta<BinaryListState>,
46
47    /// The list of test binaries.
48    pub rust_binaries: Vec<RustTestBinary>,
49}
50
51impl BinaryList {
52    /// Parses Cargo messages from the given `BufRead` and returns a list of test binaries.
53    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    /// Constructs the list from its summary format
69    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    /// Outputs this list to the given writer.
89    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    /// Outputs this list as a string with the given format.
184    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/// Incrementally builds a [`BinaryList`] from Cargo messages.
192#[derive(Debug)]
193pub struct BinaryListBuilder<'g> {
194    state: BinaryListBuildState<'g>,
195}
196
197impl<'g> BinaryListBuilder<'g> {
198    /// Creates a new builder for Cargo messages.
199    pub fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
200        Self {
201            state: BinaryListBuildState::new(graph, build_platforms),
202        }
203    }
204
205    /// Processes a single Cargo message.
206    pub fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
207        self.state.process_message(message)
208    }
209
210    /// Processes a single line of Cargo output.
211    ///
212    /// This uses the same single-line parsing behavior as
213    /// [`cargo_metadata::Message::parse_stream`].
214    pub fn process_message_line(&mut self, line: &str) -> Result<(), FromMessagesError> {
215        self.process_message(parse_message_line(line))
216    }
217
218    /// Finishes building the binary list.
219    pub fn finish(self) -> BinaryList {
220        self.state.finish()
221    }
222}
223
224// Adapted from cargo_metadata::MessageIter::next (cargo_metadata 0.23.1).
225fn 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        // For testing only, not part of the public API.
243        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                // Ignore all other messages.
265            }
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                // Look up the executable by package ID.
279
280                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                    // https://doc.rust-lang.org/nightly/cargo/reference/cargo-targets.html#the-crate-type-field
297                    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                    // Non-lib kinds should always have just one element. Grab the first one.
311                    (
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                // Construct the binary ID from the package and build target.
323                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                // This is a non-test binary -- add it to the map.
340                // Error case here implies that the returned path wasn't in the target directory -- ignore it
341                // since it shouldn't happen in normal use.
342                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            // Also look for and grab dynamic libraries to store in archives.
363            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    /// Look for paths that contain "deps" in their second-to-last component,
384    /// and are descendants of the target directory.
385    /// The paths without "deps" are base output directories.
386    ///
387    /// e.g. path/to/repo/target/debug/deps/test-binary => add "debug"
388    /// to base output dirs.
389    ///
390    /// Note that test binaries are always present in "deps", so we should always
391    /// have a match.
392    ///
393    /// The `Option` in the return value is to let ? work.
394    fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
395        // Artifact paths must be relative to the target directory.
396        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        // We only care about build scripts for workspace packages.
417        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 about processing a package that isn't in the package graph.
421                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            // Ignore this build script if it's not in the target directory.
432            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    /// The `Option` in the return value is to let ? work.
447    fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
448        // Remove anything up to the first "=" (e.g. "native=").
449        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                // For a seeded build (like in our test suite), Cargo will
458                // return:
459                //
460                // * the new path if the linked path exists
461                // * the original path if the linked path does not exist
462                //
463                // Linked paths not existing is not an ordinary condition, but
464                // we want to test it within nextest. We filter out paths if
465                // they're not a subdirectory of the target directory. With
466                // __NEXTEST_ALT_TARGET_DIR, we can simulate that for an
467                // alternate target directory.
468                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        // Clean out any build script output directories for which there's no corresponding binary.
489        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                // Test out the error case for unavailable libdirs.
559                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        // Check that the expected outputs are valid.
594        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        // Non-verbose oneline is the same as non-verbose human.
686        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}