1 // Copyright (C) 2023 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! Types for parsing cargo.metadata JSON files.
16 
17 use super::{Crate, CrateType, Extern, ExternType};
18 use crate::config::VariantConfig;
19 use anyhow::{bail, Context, Result};
20 use serde::Deserialize;
21 use std::collections::BTreeMap;
22 use std::path::{Path, PathBuf};
23 
24 /// `cfg` strings for dependencies which should be considered enabled. It would be better to parse
25 /// them properly, but this is good enough in practice so far.
26 const ENABLED_CFGS: [&str; 6] = [
27     r#"unix"#,
28     r#"not(windows)"#,
29     r#"any(unix, target_os = "wasi")"#,
30     r#"not(all(target_family = "wasm", target_os = "unknown"))"#,
31     r#"not(target_family = "wasm")"#,
32     r#"any(target_os = "linux", target_os = "android")"#,
33 ];
34 
35 /// `cargo metadata` output.
36 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
37 pub struct WorkspaceMetadata {
38     pub packages: Vec<PackageMetadata>,
39     pub workspace_members: Vec<String>,
40 }
41 
42 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
43 pub struct PackageMetadata {
44     pub name: String,
45     pub version: String,
46     pub edition: String,
47     pub manifest_path: String,
48     pub dependencies: Vec<DependencyMetadata>,
49     pub features: BTreeMap<String, Vec<String>>,
50     pub id: String,
51     pub targets: Vec<TargetMetadata>,
52 }
53 
54 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
55 pub struct DependencyMetadata {
56     pub name: String,
57     pub kind: Option<String>,
58     pub optional: bool,
59     pub target: Option<String>,
60     pub rename: Option<String>,
61 }
62 
63 impl DependencyMetadata {
64     /// Returns whether the dependency should be included when the given features are enabled.
enabled(&self, features: &[String], cfgs: &[String]) -> bool65     fn enabled(&self, features: &[String], cfgs: &[String]) -> bool {
66         if let Some(target) = &self.target {
67             if target.starts_with("cfg(") && target.ends_with(')') {
68                 let target_cfg = &target[4..target.len() - 1];
69                 if !ENABLED_CFGS.contains(&target_cfg) && !cfgs.contains(&target_cfg.to_string()) {
70                     return false;
71                 }
72             }
73         }
74         let name = self.rename.as_ref().unwrap_or(&self.name);
75         !self.optional || features.contains(&format!("dep:{}", name))
76     }
77 }
78 
79 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
80 #[allow(dead_code)]
81 pub struct TargetMetadata {
82     pub crate_types: Vec<CrateType>,
83     pub doc: bool,
84     pub doctest: bool,
85     pub edition: String,
86     pub kind: Vec<TargetKind>,
87     pub name: String,
88     pub src_path: PathBuf,
89     pub test: bool,
90 }
91 
92 #[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
93 #[serde[rename_all = "kebab-case"]]
94 pub enum TargetKind {
95     Bin,
96     CustomBuild,
97     Bench,
98     Example,
99     Lib,
100     Rlib,
101     Staticlib,
102     Cdylib,
103     ProcMacro,
104     Test,
105 }
106 
parse_cargo_metadata_str(cargo_metadata: &str, cfg: &VariantConfig) -> Result<Vec<Crate>>107 pub fn parse_cargo_metadata_str(cargo_metadata: &str, cfg: &VariantConfig) -> Result<Vec<Crate>> {
108     let metadata =
109         serde_json::from_str(cargo_metadata).context("failed to parse cargo metadata")?;
110     parse_cargo_metadata(&metadata, &cfg.features, &cfg.extra_cfg, cfg.tests)
111 }
112 
parse_cargo_metadata( metadata: &WorkspaceMetadata, features: &Option<Vec<String>>, cfgs: &[String], include_tests: bool, ) -> Result<Vec<Crate>>113 fn parse_cargo_metadata(
114     metadata: &WorkspaceMetadata,
115     features: &Option<Vec<String>>,
116     cfgs: &[String],
117     include_tests: bool,
118 ) -> Result<Vec<Crate>> {
119     let mut crates = Vec::new();
120     for package in &metadata.packages {
121         if !metadata.workspace_members.contains(&package.id) {
122             continue;
123         }
124 
125         let features = resolve_features(features, &package.features, &package.dependencies);
126         let features_without_deps: Vec<String> =
127             features.clone().into_iter().filter(|feature| !feature.starts_with("dep:")).collect();
128         let package_dir = package_dir_from_id(&package.id)?;
129 
130         for target in &package.targets {
131             let target_kinds = target
132                 .kind
133                 .clone()
134                 .into_iter()
135                 .filter(|kind| {
136                     [
137                         TargetKind::Bin,
138                         TargetKind::Cdylib,
139                         TargetKind::Lib,
140                         TargetKind::ProcMacro,
141                         TargetKind::Rlib,
142                         TargetKind::Staticlib,
143                         TargetKind::Test,
144                     ]
145                     .contains(kind)
146                 })
147                 .collect::<Vec<_>>();
148             if target_kinds.is_empty() {
149                 // Only binaries, libraries and integration tests are supported.
150                 continue;
151             }
152             let main_src = split_src_path(&target.src_path, &package_dir);
153             // Hypens are not allowed in crate names. See
154             // https://github.com/rust-lang/rfcs/blob/master/text/0940-hyphens-considered-harmful.md
155             // for background.
156             let target_name = target.name.replace('-', "_");
157             let target_triple = if target_kinds == [TargetKind::ProcMacro] {
158                 None
159             } else {
160                 Some("x86_64-unknown-linux-gnu".to_string())
161             };
162             // Don't generate an entry for integration tests, they will be covered by the test case
163             // below.
164             if target_kinds != [TargetKind::Test] {
165                 crates.push(Crate {
166                     name: target_name.clone(),
167                     package_name: package.name.to_owned(),
168                     version: Some(package.version.to_owned()),
169                     types: target.crate_types.clone(),
170                     features: features_without_deps.clone(),
171                     edition: package.edition.to_owned(),
172                     package_dir: package_dir.clone(),
173                     main_src: main_src.to_owned(),
174                     target: target_triple.clone(),
175                     externs: get_externs(
176                         package,
177                         &metadata.packages,
178                         &features,
179                         cfgs,
180                         &target_kinds,
181                         false,
182                     )?,
183                     cfgs: cfgs.to_owned(),
184                     ..Default::default()
185                 });
186             }
187             // This includes both unit tests and integration tests.
188             if target.test && include_tests {
189                 crates.push(Crate {
190                     name: target_name,
191                     package_name: package.name.to_owned(),
192                     version: Some(package.version.to_owned()),
193                     types: vec![CrateType::Test],
194                     features: features_without_deps.clone(),
195                     edition: package.edition.to_owned(),
196                     package_dir: package_dir.clone(),
197                     main_src: main_src.to_owned(),
198                     target: target_triple.clone(),
199                     externs: get_externs(
200                         package,
201                         &metadata.packages,
202                         &features,
203                         cfgs,
204                         &target_kinds,
205                         true,
206                     )?,
207                     cfgs: cfgs.to_owned(),
208                     ..Default::default()
209                 });
210             }
211         }
212     }
213     Ok(crates)
214 }
215 
get_externs( package: &PackageMetadata, packages: &[PackageMetadata], features: &[String], cfgs: &[String], target_kinds: &[TargetKind], test: bool, ) -> Result<Vec<Extern>>216 fn get_externs(
217     package: &PackageMetadata,
218     packages: &[PackageMetadata],
219     features: &[String],
220     cfgs: &[String],
221     target_kinds: &[TargetKind],
222     test: bool,
223 ) -> Result<Vec<Extern>> {
224     let mut externs = package
225         .dependencies
226         .iter()
227         .filter_map(|dependency| {
228             // Kind is None for normal dependencies, as opposed to dev dependencies.
229             if dependency.enabled(features, cfgs)
230                 && dependency.kind.as_deref() != Some("build")
231                 && (dependency.kind.is_none() || test)
232             {
233                 Some(make_extern(packages, dependency))
234             } else {
235                 None
236             }
237         })
238         .collect::<Result<Vec<Extern>>>()?;
239 
240     // If there is a library target and this is a binary or integration test, add the library as an
241     // extern.
242     if matches!(target_kinds, [TargetKind::Bin] | [TargetKind::Test]) {
243         for target in &package.targets {
244             if target.kind.contains(&TargetKind::Lib) {
245                 let lib_name = target.name.replace('-', "_");
246                 externs.push(Extern {
247                     name: lib_name.clone(),
248                     lib_name,
249                     extern_type: ExternType::Rust,
250                 });
251             }
252         }
253     }
254 
255     externs.sort();
256     externs.dedup();
257     Ok(externs)
258 }
259 
make_extern(packages: &[PackageMetadata], dependency: &DependencyMetadata) -> Result<Extern>260 fn make_extern(packages: &[PackageMetadata], dependency: &DependencyMetadata) -> Result<Extern> {
261     let Some(package) = packages.iter().find(|package| package.name == dependency.name) else {
262         bail!("package {} not found in metadata", dependency.name);
263     };
264     let Some(target) = package.targets.iter().find(|target| {
265         target.kind.contains(&TargetKind::Lib) || target.kind.contains(&TargetKind::ProcMacro)
266     }) else {
267         bail!("Package {} didn't have any library or proc-macro targets", dependency.name);
268     };
269     let lib_name = target.name.replace('-', "_");
270     let name =
271         if let Some(rename) = &dependency.rename { rename.clone() } else { lib_name.clone() };
272 
273     // Check whether the package is a proc macro.
274     let extern_type =
275         if package.targets.iter().any(|target| target.kind.contains(&TargetKind::ProcMacro)) {
276             ExternType::ProcMacro
277         } else {
278             ExternType::Rust
279         };
280 
281     Ok(Extern { name, lib_name, extern_type })
282 }
283 
284 /// Given a Cargo package ID, returns the path.
285 ///
286 /// Extracts `"/path/to/crate"` from
287 /// `"path+file:///path/to/crate#1.2.3"`. See
288 /// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html for
289 /// information on Cargo package ID specifications.
package_dir_from_id(id: &str) -> Result<PathBuf>290 fn package_dir_from_id(id: &str) -> Result<PathBuf> {
291     const PREFIX: &str = "path+file://";
292     const SEPARATOR: char = '#';
293     let Some(stripped) = id.strip_prefix(PREFIX) else {
294         bail!("Invalid package ID {id:?}, expected it to start with {PREFIX:?}");
295     };
296     let Some(idx) = stripped.rfind(SEPARATOR) else {
297         bail!("Invalid package ID {id:?}, expected it to contain {SEPARATOR:?}");
298     };
299     Ok(PathBuf::from(stripped[..idx].to_string()))
300 }
301 
split_src_path<'a>(src_path: &'a Path, package_dir: &Path) -> &'a Path302 fn split_src_path<'a>(src_path: &'a Path, package_dir: &Path) -> &'a Path {
303     if let Ok(main_src) = src_path.strip_prefix(package_dir) {
304         main_src
305     } else {
306         src_path
307     }
308 }
309 
310 /// Given a set of chosen features, and the feature dependencies from a package's metadata, returns
311 /// the full set of features which should be enabled.
resolve_features( chosen_features: &Option<Vec<String>>, package_features: &BTreeMap<String, Vec<String>>, dependencies: &[DependencyMetadata], ) -> Vec<String>312 fn resolve_features(
313     chosen_features: &Option<Vec<String>>,
314     package_features: &BTreeMap<String, Vec<String>>,
315     dependencies: &[DependencyMetadata],
316 ) -> Vec<String> {
317     let mut package_features = package_features.to_owned();
318     // Add implicit features for optional dependencies.
319     for dependency in dependencies {
320         if dependency.optional && !package_features.contains_key(&dependency.name) {
321             package_features
322                 .insert(dependency.name.to_owned(), vec![format!("dep:{}", dependency.name)]);
323         }
324     }
325 
326     let mut features = Vec::new();
327     if let Some(chosen_features) = chosen_features {
328         for feature in chosen_features {
329             add_feature_and_dependencies(&mut features, feature, &package_features);
330         }
331     } else {
332         // If there are no chosen features, then enable the default feature.
333         add_feature_and_dependencies(&mut features, "default", &package_features);
334     }
335     features.sort();
336     features.dedup();
337     features
338 }
339 
340 /// Adds the given feature and all features it depends on to the given list of features.
341 ///
342 /// Ignores features of other packages, and features which don't exist.
add_feature_and_dependencies( features: &mut Vec<String>, feature: &str, package_features: &BTreeMap<String, Vec<String>>, )343 fn add_feature_and_dependencies(
344     features: &mut Vec<String>,
345     feature: &str,
346     package_features: &BTreeMap<String, Vec<String>>,
347 ) {
348     if package_features.contains_key(feature) || feature.starts_with("dep:") {
349         features.push(feature.to_owned());
350     }
351 
352     if let Some(dependencies) = package_features.get(feature) {
353         for dependency in dependencies {
354             if let Some((dependency_package, _)) = dependency.split_once('/') {
355                 add_feature_and_dependencies(features, dependency_package, package_features);
356             } else {
357                 add_feature_and_dependencies(features, dependency, package_features);
358             }
359         }
360     }
361 }
362 
363 #[cfg(test)]
364 mod tests {
365     use super::*;
366     use crate::config::Config;
367     use crate::tests::testdata_directories;
368     use googletest::matchers::eq;
369     use googletest::prelude::assert_that;
370     use std::fs::{read_to_string, File};
371 
372     #[test]
extract_package_dir_from_id() -> Result<()>373     fn extract_package_dir_from_id() -> Result<()> {
374         assert_eq!(
375             package_dir_from_id("path+file:///path/to/crate#1.2.3")?,
376             PathBuf::from("/path/to/crate")
377         );
378         Ok(())
379     }
380 
381     #[test]
resolve_multi_level_feature_dependencies()382     fn resolve_multi_level_feature_dependencies() {
383         let chosen = vec!["default".to_string(), "extra".to_string(), "on_by_default".to_string()];
384         let package_features = [
385             (
386                 "default".to_string(),
387                 vec!["std".to_string(), "other".to_string(), "on_by_default".to_string()],
388             ),
389             ("std".to_string(), vec!["alloc".to_string()]),
390             ("not_enabled".to_string(), vec![]),
391             ("on_by_default".to_string(), vec![]),
392             ("other".to_string(), vec![]),
393             ("extra".to_string(), vec![]),
394             ("alloc".to_string(), vec![]),
395         ]
396         .into_iter()
397         .collect();
398         assert_eq!(
399             resolve_features(&Some(chosen), &package_features, &[]),
400             vec![
401                 "alloc".to_string(),
402                 "default".to_string(),
403                 "extra".to_string(),
404                 "on_by_default".to_string(),
405                 "other".to_string(),
406                 "std".to_string(),
407             ]
408         );
409     }
410 
411     #[test]
resolve_dep_features()412     fn resolve_dep_features() {
413         let package_features = [(
414             "default".to_string(),
415             vec![
416                 "optionaldep/feature".to_string(),
417                 "requireddep/feature".to_string(),
418                 "optionaldep2?/feature".to_string(),
419             ],
420         )]
421         .into_iter()
422         .collect();
423         let dependencies = vec![
424             DependencyMetadata {
425                 name: "optionaldep".to_string(),
426                 kind: None,
427                 optional: true,
428                 target: None,
429                 rename: None,
430             },
431             DependencyMetadata {
432                 name: "optionaldep2".to_string(),
433                 kind: None,
434                 optional: true,
435                 target: None,
436                 rename: None,
437             },
438             DependencyMetadata {
439                 name: "requireddep".to_string(),
440                 kind: None,
441                 optional: false,
442                 target: None,
443                 rename: None,
444             },
445         ];
446         assert_eq!(
447             resolve_features(&None, &package_features, &dependencies),
448             vec!["default".to_string(), "dep:optionaldep".to_string(), "optionaldep".to_string()]
449         );
450     }
451 
452     #[test]
get_externs_cfg()453     fn get_externs_cfg() {
454         let package = PackageMetadata {
455             name: "test_package".to_string(),
456             dependencies: vec![
457                 DependencyMetadata {
458                     name: "alwayslib".to_string(),
459                     kind: None,
460                     optional: false,
461                     target: None,
462                     rename: None,
463                 },
464                 DependencyMetadata {
465                     name: "unixlib".to_string(),
466                     kind: None,
467                     optional: false,
468                     target: Some("cfg(unix)".to_string()),
469                     rename: None,
470                 },
471                 DependencyMetadata {
472                     name: "windowslib".to_string(),
473                     kind: None,
474                     optional: false,
475                     target: Some("cfg(windows)".to_string()),
476                     rename: None,
477                 },
478             ],
479             features: [].into_iter().collect(),
480             targets: vec![],
481             ..Default::default()
482         };
483         let packages = vec![
484             package.clone(),
485             PackageMetadata {
486                 name: "alwayslib".to_string(),
487                 targets: vec![TargetMetadata {
488                     name: "alwayslib".to_string(),
489                     kind: vec![TargetKind::Lib],
490                     ..Default::default()
491                 }],
492                 ..Default::default()
493             },
494             PackageMetadata {
495                 name: "unixlib".to_string(),
496                 targets: vec![TargetMetadata {
497                     name: "unixlib".to_string(),
498                     kind: vec![TargetKind::Lib],
499                     ..Default::default()
500                 }],
501                 ..Default::default()
502             },
503             PackageMetadata {
504                 name: "windowslib".to_string(),
505                 targets: vec![TargetMetadata {
506                     name: "windowslib".to_string(),
507                     kind: vec![TargetKind::Lib],
508                     ..Default::default()
509                 }],
510                 ..Default::default()
511             },
512         ];
513         assert_eq!(
514             get_externs(&package, &packages, &[], &[], &[], false).unwrap(),
515             vec![
516                 Extern {
517                     name: "alwayslib".to_string(),
518                     lib_name: "alwayslib".to_string(),
519                     extern_type: ExternType::Rust
520                 },
521                 Extern {
522                     name: "unixlib".to_string(),
523                     lib_name: "unixlib".to_string(),
524                     extern_type: ExternType::Rust
525                 },
526             ]
527         );
528     }
529 
530     #[test]
get_externs_extra_cfg()531     fn get_externs_extra_cfg() {
532         let package = PackageMetadata {
533             name: "test_package".to_string(),
534             dependencies: vec![
535                 DependencyMetadata {
536                     name: "foolib".to_string(),
537                     kind: None,
538                     optional: false,
539                     target: Some("cfg(foo)".to_string()),
540                     rename: None,
541                 },
542                 DependencyMetadata {
543                     name: "barlib".to_string(),
544                     kind: None,
545                     optional: false,
546                     target: Some("cfg(bar)".to_string()),
547                     rename: None,
548                 },
549             ],
550             features: [].into_iter().collect(),
551             targets: vec![],
552             ..Default::default()
553         };
554         let packages = vec![
555             package.clone(),
556             PackageMetadata {
557                 name: "foolib".to_string(),
558                 targets: vec![TargetMetadata {
559                     name: "foolib".to_string(),
560                     kind: vec![TargetKind::Lib],
561                     ..Default::default()
562                 }],
563                 ..Default::default()
564             },
565             PackageMetadata {
566                 name: "barlib".to_string(),
567                 targets: vec![TargetMetadata {
568                     name: "barlib".to_string(),
569                     kind: vec![TargetKind::Lib],
570                     ..Default::default()
571                 }],
572                 ..Default::default()
573             },
574         ];
575         assert_eq!(
576             get_externs(&package, &packages, &[], &["foo".to_string()], &[], false).unwrap(),
577             vec![Extern {
578                 name: "foolib".to_string(),
579                 lib_name: "foolib".to_string(),
580                 extern_type: ExternType::Rust
581             },]
582         );
583     }
584 
585     #[test]
get_externs_rename()586     fn get_externs_rename() {
587         let package = PackageMetadata {
588             name: "test_package".to_string(),
589             dependencies: vec![
590                 DependencyMetadata {
591                     name: "foo".to_string(),
592                     kind: None,
593                     optional: false,
594                     target: None,
595                     rename: Some("foo2".to_string()),
596                 },
597                 DependencyMetadata {
598                     name: "bar".to_string(),
599                     kind: None,
600                     optional: true,
601                     target: None,
602                     rename: None,
603                 },
604                 DependencyMetadata {
605                     name: "bar".to_string(),
606                     kind: None,
607                     optional: true,
608                     target: None,
609                     rename: Some("baz".to_string()),
610                 },
611             ],
612             ..Default::default()
613         };
614         let packages = vec![
615             package.clone(),
616             PackageMetadata {
617                 name: "foo".to_string(),
618                 targets: vec![TargetMetadata {
619                     name: "foo".to_string(),
620                     kind: vec![TargetKind::Lib],
621                     ..Default::default()
622                 }],
623                 ..Default::default()
624             },
625             PackageMetadata {
626                 name: "bar".to_string(),
627                 targets: vec![TargetMetadata {
628                     name: "bar".to_string(),
629                     kind: vec![TargetKind::Lib],
630                     ..Default::default()
631                 }],
632                 ..Default::default()
633             },
634         ];
635         assert_eq!(
636             get_externs(&package, &packages, &["dep:bar".to_string()], &[], &[], false).unwrap(),
637             vec![
638                 Extern {
639                     name: "bar".to_string(),
640                     lib_name: "bar".to_string(),
641                     extern_type: ExternType::Rust
642                 },
643                 Extern {
644                     name: "foo2".to_string(),
645                     lib_name: "foo".to_string(),
646                     extern_type: ExternType::Rust
647                 },
648             ]
649         );
650         assert_eq!(
651             get_externs(&package, &packages, &["dep:baz".to_string()], &[], &[], false).unwrap(),
652             vec![
653                 Extern {
654                     name: "baz".to_string(),
655                     lib_name: "bar".to_string(),
656                     extern_type: ExternType::Rust
657                 },
658                 Extern {
659                     name: "foo2".to_string(),
660                     lib_name: "foo".to_string(),
661                     extern_type: ExternType::Rust
662                 },
663             ]
664         );
665     }
666 
667     #[test]
parse_metadata()668     fn parse_metadata() {
669         /// Remove anything before "external/rust/crates/" from the
670         /// `package_dir` field. This makes the test robust since you
671         /// can use `cargo metadata` to regenerate the test files and
672         /// you don't have to care about where your AOSP checkout
673         /// lives.
674         fn normalize_package_dir(mut c: Crate) -> Crate {
675             const EXTERNAL_RUST_CRATES: &str = "external/rust/crates/";
676             let package_dir = c.package_dir.to_str().unwrap();
677             if let Some(idx) = package_dir.find(EXTERNAL_RUST_CRATES) {
678                 c.package_dir = PathBuf::from(format!(".../{}", &package_dir[idx..]));
679             }
680             c
681         }
682 
683         for testdata_directory_path in testdata_directories() {
684             let cfg = Config::from_json_str(
685                 &read_to_string(testdata_directory_path.join("cargo_embargo.json"))
686                     .with_context(|| {
687                         format!(
688                             "Failed to open {:?}",
689                             testdata_directory_path.join("cargo_embargo.json")
690                         )
691                     })
692                     .unwrap(),
693             )
694             .unwrap();
695             let cargo_metadata_path = testdata_directory_path.join("cargo.metadata");
696             let expected_crates: Vec<Vec<Crate>> = serde_json::from_reader::<_, Vec<Vec<Crate>>>(
697                 File::open(testdata_directory_path.join("crates.json")).unwrap(),
698             )
699             .unwrap()
700             .into_iter()
701             .map(|crates: Vec<Crate>| crates.into_iter().map(normalize_package_dir).collect())
702             .collect();
703 
704             let crates = cfg
705                 .variants
706                 .iter()
707                 .map(|variant_cfg| {
708                     parse_cargo_metadata_str(
709                         &read_to_string(&cargo_metadata_path)
710                             .with_context(|| format!("Failed to open {:?}", cargo_metadata_path))
711                             .unwrap(),
712                         variant_cfg,
713                     )
714                     .unwrap()
715                     .into_iter()
716                     .map(normalize_package_dir)
717                     .collect::<Vec<Crate>>()
718                 })
719                 .collect::<Vec<Vec<Crate>>>();
720             assert_that!(format!("{crates:#?}"), eq(format!("{expected_crates:#?}")));
721         }
722     }
723 }
724