xref: /aosp_15_r20/external/bazelbuild-rules_rust/crate_universe/src/metadata/metadata_annotation.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1 //! Collect and store information from Cargo metadata specific to Bazel's needs
2 
3 use std::collections::{BTreeMap, BTreeSet};
4 use std::path::PathBuf;
5 
6 use anyhow::{bail, Result};
7 use cargo_metadata::{Node, Package, PackageId};
8 use hex::ToHex;
9 use serde::{Deserialize, Serialize};
10 
11 use crate::config::{Commitish, Config, CrateAnnotations, CrateId};
12 use crate::metadata::dependency::DependencySet;
13 use crate::metadata::TreeResolverMetadata;
14 use crate::splicing::{SourceInfo, WorkspaceMetadata};
15 
16 pub(crate) type CargoMetadata = cargo_metadata::Metadata;
17 pub(crate) type CargoLockfile = cargo_lock::Lockfile;
18 
19 /// Additional information about a crate relative to other crates in a dependency graph.
20 #[derive(Debug, Serialize, Deserialize)]
21 pub(crate) struct CrateAnnotation {
22     /// The crate's node in the Cargo "resolve" graph.
23     pub(crate) node: Node,
24 
25     /// The crate's sorted dependencies.
26     pub(crate) deps: DependencySet,
27 }
28 
29 /// Additional information about a Cargo workspace's metadata.
30 #[derive(Debug, Default, Serialize, Deserialize)]
31 pub(crate) struct MetadataAnnotation {
32     /// All packages found within the Cargo metadata
33     pub(crate) packages: BTreeMap<PackageId, Package>,
34 
35     /// All [CrateAnnotation]s for all packages
36     pub(crate) crates: BTreeMap<PackageId, CrateAnnotation>,
37 
38     /// All packages that are workspace members
39     pub(crate) workspace_members: BTreeSet<PackageId>,
40 
41     /// The path to the directory containing the Cargo workspace that produced the metadata.
42     pub(crate) workspace_root: PathBuf,
43 
44     /// Information on the Cargo workspace.
45     pub(crate) workspace_metadata: WorkspaceMetadata,
46 }
47 
48 impl MetadataAnnotation {
new(metadata: CargoMetadata) -> MetadataAnnotation49     pub(crate) fn new(metadata: CargoMetadata) -> MetadataAnnotation {
50         // UNWRAP: The workspace metadata should be written by a controlled process. This should not return a result
51         let workspace_metadata = find_workspace_metadata(&metadata).unwrap_or_default();
52 
53         let resolve = metadata
54             .resolve
55             .as_ref()
56             .expect("The metadata provided requires a resolve graph")
57             .clone();
58 
59         let is_node_workspace_member = |node: &Node, metadata: &CargoMetadata| -> bool {
60             metadata.workspace_members.iter().any(|pkg| pkg == &node.id)
61         };
62 
63         let workspace_members: BTreeSet<PackageId> = resolve
64             .nodes
65             .iter()
66             .filter(|node| is_node_workspace_member(node, &metadata))
67             .map(|node| node.id.clone())
68             .collect();
69 
70         let crates = resolve
71             .nodes
72             .iter()
73             .map(|node| {
74                 (
75                     node.id.clone(),
76                     Self::annotate_crate(
77                         node.clone(),
78                         &metadata,
79                         &workspace_metadata.tree_metadata,
80                     ),
81                 )
82             })
83             .collect();
84 
85         let packages = metadata
86             .packages
87             .into_iter()
88             .map(|pkg| (pkg.id.clone(), pkg))
89             .collect();
90 
91         MetadataAnnotation {
92             packages,
93             crates,
94             workspace_members,
95             workspace_root: PathBuf::from(metadata.workspace_root.as_std_path()),
96             workspace_metadata,
97         }
98     }
99 
annotate_crate( node: Node, metadata: &CargoMetadata, resolver_data: &TreeResolverMetadata, ) -> CrateAnnotation100     fn annotate_crate(
101         node: Node,
102         metadata: &CargoMetadata,
103         resolver_data: &TreeResolverMetadata,
104     ) -> CrateAnnotation {
105         // Gather all dependencies
106         let deps = DependencySet::new_for_node(&node, metadata, resolver_data);
107 
108         CrateAnnotation { node, deps }
109     }
110 }
111 
112 /// Additional information about how and where to acquire a crate's source code from.
113 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
114 pub(crate) enum SourceAnnotation {
115     Git {
116         /// The Git url where to clone the source from.
117         remote: String,
118 
119         /// The revision information for the git repository. This is used for
120         /// [git_repository::commit](https://docs.bazel.build/versions/main/repo/git.html#git_repository-commit),
121         /// [git_repository::tag](https://docs.bazel.build/versions/main/repo/git.html#git_repository-tag), or
122         /// [git_repository::branch](https://docs.bazel.build/versions/main/repo/git.html#git_repository-branch).
123         commitish: Commitish,
124 
125         /// See [git_repository::shallow_since](https://docs.bazel.build/versions/main/repo/git.html#git_repository-shallow_since)
126         #[serde(default, skip_serializing_if = "Option::is_none")]
127         shallow_since: Option<String>,
128 
129         /// See [git_repository::strip_prefix](https://docs.bazel.build/versions/main/repo/git.html#git_repository-strip_prefix)
130         #[serde(default, skip_serializing_if = "Option::is_none")]
131         strip_prefix: Option<String>,
132 
133         /// See [git_repository::patch_args](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patch_args)
134         #[serde(default, skip_serializing_if = "Option::is_none")]
135         patch_args: Option<Vec<String>>,
136 
137         /// See [git_repository::patch_tool](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patch_tool)
138         #[serde(default, skip_serializing_if = "Option::is_none")]
139         patch_tool: Option<String>,
140 
141         /// See [git_repository::patches](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patches)
142         #[serde(default, skip_serializing_if = "Option::is_none")]
143         patches: Option<BTreeSet<String>>,
144     },
145     Http {
146         /// See [http_archive::url](https://docs.bazel.build/versions/main/repo/http.html#http_archive-url)
147         url: String,
148 
149         /// See [http_archive::sha256](https://docs.bazel.build/versions/main/repo/http.html#http_archive-sha256)
150         #[serde(default, skip_serializing_if = "Option::is_none")]
151         sha256: Option<String>,
152 
153         /// See [http_archive::patch_args](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patch_args)
154         #[serde(default, skip_serializing_if = "Option::is_none")]
155         patch_args: Option<Vec<String>>,
156 
157         /// See [http_archive::patch_tool](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patch_tool)
158         #[serde(default, skip_serializing_if = "Option::is_none")]
159         patch_tool: Option<String>,
160 
161         /// See [http_archive::patches](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patches)
162         #[serde(default, skip_serializing_if = "Option::is_none")]
163         patches: Option<BTreeSet<String>>,
164     },
165 }
166 
167 /// Additional information related to [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html)
168 /// data used for improved determinism.
169 #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
170 pub(crate) struct LockfileAnnotation {
171     /// A mapping of crates/packages to additional source (network location) information.
172     pub(crate) crates: BTreeMap<PackageId, SourceAnnotation>,
173 }
174 
175 impl LockfileAnnotation {
new(lockfile: CargoLockfile, metadata: &CargoMetadata) -> Result<Self>176     pub(crate) fn new(lockfile: CargoLockfile, metadata: &CargoMetadata) -> Result<Self> {
177         let workspace_metadata = find_workspace_metadata(metadata).unwrap_or_default();
178 
179         let nodes: Vec<&Node> = metadata
180             .resolve
181             .as_ref()
182             .expect("Metadata is expected to have a resolve graph")
183             .nodes
184             .iter()
185             .filter(|node| !is_workspace_member(&node.id, metadata))
186             .collect();
187 
188         // Produce source annotations for each crate in the resolve graph
189         let crates = nodes
190             .iter()
191             .map(|node| {
192                 Ok((
193                     node.id.clone(),
194                     Self::collect_source_annotations(
195                         node,
196                         metadata,
197                         &lockfile,
198                         &workspace_metadata,
199                     )?,
200                 ))
201             })
202             .collect::<Result<BTreeMap<PackageId, SourceAnnotation>>>()?;
203 
204         Ok(Self { crates })
205     }
206 
207     /// Resolve all URLs and checksum-like data for each package
collect_source_annotations( node: &Node, metadata: &CargoMetadata, lockfile: &CargoLockfile, workspace_metadata: &WorkspaceMetadata, ) -> Result<SourceAnnotation>208     fn collect_source_annotations(
209         node: &Node,
210         metadata: &CargoMetadata,
211         lockfile: &CargoLockfile,
212         workspace_metadata: &WorkspaceMetadata,
213     ) -> Result<SourceAnnotation> {
214         let pkg = &metadata[&node.id];
215 
216         // Locate the matching lock package for the current crate
217         let lock_pkg = match cargo_meta_pkg_to_locked_pkg(pkg, &lockfile.packages) {
218             Some(lock_pkg) => lock_pkg,
219             None => bail!(
220                 "Could not find lockfile entry matching metadata package '{}'",
221                 pkg.name
222             ),
223         };
224 
225         // Check for spliced information about a crate's network source.
226         let spliced_source_info = Self::find_source_annotation(lock_pkg, workspace_metadata);
227 
228         // Parse it's source info. The check above should prevent a panic
229         let source = match lock_pkg.source.as_ref() {
230             Some(source) => source,
231             None => match spliced_source_info {
232                 Some(info) => {
233                     return Ok(SourceAnnotation::Http {
234                         url: info.url,
235                         sha256: Some(info.sha256),
236                         patch_args: None,
237                         patch_tool: None,
238                         patches: None,
239                     })
240                 }
241                 None => bail!(
242                     "The package '{:?} {:?}' has no source info so no annotation can be made",
243                     lock_pkg.name,
244                     lock_pkg.version
245                 ),
246             },
247         };
248 
249         // Handle any git repositories
250         if let Some(git_ref) = source.git_reference() {
251             let strip_prefix = Self::extract_git_strip_prefix(pkg)?;
252 
253             return Ok(SourceAnnotation::Git {
254                 remote: source.url().to_string(),
255                 commitish: source
256                     .precise()
257                     .map(|rev| Commitish::Rev(rev.to_string()))
258                     .unwrap_or(Commitish::from(git_ref.clone())),
259                 shallow_since: None,
260                 strip_prefix,
261                 patch_args: None,
262                 patch_tool: None,
263                 patches: None,
264             });
265         }
266 
267         // One of the last things that should be checked is the spliced source information as
268         // other sources may more accurately represent where a crate should be downloaded.
269         if let Some(info) = spliced_source_info {
270             return Ok(SourceAnnotation::Http {
271                 url: info.url,
272                 sha256: Some(info.sha256),
273                 patch_args: None,
274                 patch_tool: None,
275                 patches: None,
276             });
277         }
278 
279         // Finally, In the event that no spliced source information was included in the
280         // metadata the raw source info is used for registry crates and `crates.io` is
281         // assumed to be the source.
282         if source.is_registry() {
283             // source url
284             return Ok(SourceAnnotation::Http {
285                 url: format!(
286                     "https://static.crates.io/crates/{}/{}/download",
287                     lock_pkg.name, lock_pkg.version
288                 ),
289                 sha256: lock_pkg
290                     .checksum
291                     .as_ref()
292                     .and_then(|sum| {
293                         if sum.is_sha256() {
294                             sum.as_sha256()
295                         } else {
296                             None
297                         }
298                     })
299                     .map(|sum| sum.encode_hex::<String>()),
300                 patch_args: None,
301                 patch_tool: None,
302                 patches: None,
303             });
304         }
305 
306         bail!(
307             "Unable to determine source annotation for '{:?} {:?}",
308             lock_pkg.name,
309             lock_pkg.version
310         )
311     }
312 
find_source_annotation( package: &cargo_lock::Package, metadata: &WorkspaceMetadata, ) -> Option<SourceInfo>313     fn find_source_annotation(
314         package: &cargo_lock::Package,
315         metadata: &WorkspaceMetadata,
316     ) -> Option<SourceInfo> {
317         let crate_id = CrateId::new(package.name.to_string(), package.version.clone());
318         metadata.sources.get(&crate_id).cloned()
319     }
320 
extract_git_strip_prefix(pkg: &Package) -> Result<Option<String>>321     fn extract_git_strip_prefix(pkg: &Package) -> Result<Option<String>> {
322         // {CARGO_HOME}/git/checkouts/name-hash/short-sha/[strip_prefix...]/Cargo.toml
323         let components = pkg
324             .manifest_path
325             .components()
326             .map(|v| v.to_string())
327             .collect::<Vec<_>>();
328         for (i, _) in components.iter().enumerate() {
329             let possible_components = &components[i..];
330             if possible_components.len() < 5 {
331                 continue;
332             }
333             if possible_components[0] != "git"
334                 || possible_components[1] != "checkouts"
335                 || possible_components[possible_components.len() - 1] != "Cargo.toml"
336             {
337                 continue;
338             }
339             if possible_components.len() == 5 {
340                 return Ok(None);
341             }
342             return Ok(Some(
343                 possible_components[4..(possible_components.len() - 1)].join("/"),
344             ));
345         }
346         bail!("Expected git package to have a manifest path of pattern {{CARGO_HOME}}/git/checkouts/[name]-[hash]/[short-sha]/.../Cargo.toml but {:?} had manifest path {}", pkg.id, pkg.manifest_path);
347     }
348 }
349 
350 /// A pairing of a crate's package identifier to its annotations.
351 #[derive(Debug)]
352 pub(crate) struct PairedExtras {
353     /// The crate's package identifier
354     pub(crate) package_id: cargo_metadata::PackageId,
355 
356     /// The crate's annotations
357     pub(crate) crate_extra: CrateAnnotations,
358 }
359 
360 /// A collection of data which has been processed for optimal use in generating Bazel targets.
361 #[derive(Debug, Default)]
362 pub(crate) struct Annotations {
363     /// Annotated Cargo metadata
364     pub(crate) metadata: MetadataAnnotation,
365 
366     /// Annotated Cargo lockfile
367     pub(crate) lockfile: LockfileAnnotation,
368 
369     /// The current workspace's configuration settings
370     pub(crate) config: Config,
371 
372     /// Pairred crate annotations
373     pub(crate) pairred_extras: BTreeMap<CrateId, PairedExtras>,
374 }
375 
376 impl Annotations {
new( cargo_metadata: CargoMetadata, cargo_lockfile: CargoLockfile, config: Config, ) -> Result<Self>377     pub(crate) fn new(
378         cargo_metadata: CargoMetadata,
379         cargo_lockfile: CargoLockfile,
380         config: Config,
381     ) -> Result<Self> {
382         let lockfile_annotation = LockfileAnnotation::new(cargo_lockfile, &cargo_metadata)?;
383 
384         // Annotate the cargo metadata
385         let metadata_annotation = MetadataAnnotation::new(cargo_metadata);
386 
387         let mut unused_extra_annotations = config.annotations.clone();
388 
389         // Ensure each override matches a particular package
390         let pairred_extras = metadata_annotation
391             .packages
392             .iter()
393             .filter_map(|(pkg_id, pkg)| {
394                 let mut crate_extra: CrateAnnotations = config
395                     .annotations
396                     .iter()
397                     .filter(|(id, _)| id.matches(pkg))
398                     .map(|(id, extra)| {
399                         // Mark that an annotation has been consumed
400                         unused_extra_annotations.remove(id);
401 
402                         // Filter out the annotation
403                         extra
404                     })
405                     .cloned()
406                     .sum();
407 
408                 crate_extra.apply_defaults_from_package_metadata(&pkg.metadata);
409 
410                 if crate_extra == CrateAnnotations::default() {
411                     None
412                 } else {
413                     Some((
414                         CrateId::new(pkg.name.clone(), pkg.version.clone()),
415                         PairedExtras {
416                             package_id: pkg_id.clone(),
417                             crate_extra,
418                         },
419                     ))
420                 }
421             })
422             .collect();
423 
424         // Alert on any unused annotations
425         if !unused_extra_annotations.is_empty() {
426             bail!(
427                 "Unused annotations were provided. Please remove them: {:?}",
428                 unused_extra_annotations.keys()
429             );
430         }
431 
432         // Annotate metadata
433         Ok(Annotations {
434             metadata: metadata_annotation,
435             lockfile: lockfile_annotation,
436             config,
437             pairred_extras,
438         })
439     }
440 }
441 
find_workspace_metadata(cargo_metadata: &CargoMetadata) -> Option<WorkspaceMetadata>442 fn find_workspace_metadata(cargo_metadata: &CargoMetadata) -> Option<WorkspaceMetadata> {
443     WorkspaceMetadata::try_from(cargo_metadata.workspace_metadata.clone()).ok()
444 }
445 
446 /// Determines whether or not a package is a workspace member. This follows
447 /// the Cargo definition of a workspace memeber with one exception where
448 /// "extra workspace members" are *not* treated as workspace members
is_workspace_member(id: &PackageId, cargo_metadata: &CargoMetadata) -> bool449 fn is_workspace_member(id: &PackageId, cargo_metadata: &CargoMetadata) -> bool {
450     if cargo_metadata.workspace_members.contains(id) {
451         if let Some(data) = find_workspace_metadata(cargo_metadata) {
452             let pkg = &cargo_metadata[id];
453             let crate_id = CrateId::new(pkg.name.clone(), pkg.version.clone());
454 
455             !data.sources.contains_key(&crate_id)
456         } else {
457             true
458         }
459     } else {
460         false
461     }
462 }
463 
464 /// Match a [cargo_metadata::Package] to a [cargo_lock::Package].
cargo_meta_pkg_to_locked_pkg<'a>( pkg: &Package, lock_packages: &'a [cargo_lock::Package], ) -> Option<&'a cargo_lock::Package>465 fn cargo_meta_pkg_to_locked_pkg<'a>(
466     pkg: &Package,
467     lock_packages: &'a [cargo_lock::Package],
468 ) -> Option<&'a cargo_lock::Package> {
469     lock_packages
470         .iter()
471         .find(|lock_pkg| lock_pkg.name.as_str() == pkg.name && lock_pkg.version == pkg.version)
472 }
473 
474 #[cfg(test)]
475 mod test {
476     use super::*;
477 
478     use semver::Version;
479     use serde_json::json;
480 
481     use crate::config::CrateNameAndVersionReq;
482     use crate::metadata::CargoTreeEntry;
483     use crate::select::Select;
484     use crate::test::*;
485 
486     #[test]
test_cargo_meta_pkg_to_locked_pkg()487     fn test_cargo_meta_pkg_to_locked_pkg() {
488         let pkg = mock_cargo_metadata_package();
489         let lock_pkg = mock_cargo_lock_package();
490 
491         assert!(cargo_meta_pkg_to_locked_pkg(&pkg, &vec![lock_pkg]).is_some())
492     }
493 
494     #[test]
annotate_metadata_with_aliases()495     fn annotate_metadata_with_aliases() {
496         let annotations = MetadataAnnotation::new(test::metadata::alias());
497         let log_crates: BTreeMap<&PackageId, &CrateAnnotation> = annotations
498             .crates
499             .iter()
500             .filter(|(id, _)| {
501                 let pkg = &annotations.packages[*id];
502                 pkg.name == "log"
503             })
504             .collect();
505 
506         assert_eq!(log_crates.len(), 2);
507     }
508 
509     #[test]
annotate_lockfile_with_aliases()510     fn annotate_lockfile_with_aliases() {
511         LockfileAnnotation::new(test::lockfile::alias(), &test::metadata::alias()).unwrap();
512     }
513 
514     #[test]
annotate_metadata_with_build_scripts()515     fn annotate_metadata_with_build_scripts() {
516         MetadataAnnotation::new(test::metadata::build_scripts());
517     }
518 
519     #[test]
annotate_lockfile_with_build_scripts()520     fn annotate_lockfile_with_build_scripts() {
521         LockfileAnnotation::new(
522             test::lockfile::build_scripts(),
523             &test::metadata::build_scripts(),
524         )
525         .unwrap();
526     }
527 
528     #[test]
annotate_lockfile_with_no_deps()529     fn annotate_lockfile_with_no_deps() {
530         LockfileAnnotation::new(test::lockfile::no_deps(), &test::metadata::no_deps()).unwrap();
531     }
532 
533     #[test]
detects_strip_prefix_for_git_repo()534     fn detects_strip_prefix_for_git_repo() {
535         let crates =
536             LockfileAnnotation::new(test::lockfile::git_repos(), &test::metadata::git_repos())
537                 .unwrap()
538                 .crates;
539         let tracing_core = crates
540             .iter()
541             .find(|(k, _)| k.repr.contains("#tracing-core@"))
542             .map(|(_, v)| v)
543             .unwrap();
544         match tracing_core {
545             SourceAnnotation::Git {
546                 strip_prefix: Some(strip_prefix),
547                 ..
548             } if strip_prefix == "tracing-core" => {
549                 // Matched correctly.
550             }
551             other => {
552                 panic!("Wanted SourceAnnotation::Git with strip_prefix == Some(\"tracing-core\"), got: {:?}", other);
553             }
554         }
555     }
556 
557     #[test]
resolves_commit_from_branches_and_tags()558     fn resolves_commit_from_branches_and_tags() {
559         let crates =
560             LockfileAnnotation::new(test::lockfile::git_repos(), &test::metadata::git_repos())
561                 .unwrap()
562                 .crates;
563 
564         let package_id = PackageId {
565             repr: "git+https://github.com/tokio-rs/tracing.git?branch=master#[email protected]".into(),
566         };
567         let annotation = crates.get(&package_id).unwrap();
568 
569         let commitish = match annotation {
570             SourceAnnotation::Git { commitish, .. } => commitish,
571             _ => panic!("Unexpected annotation type"),
572         };
573 
574         assert_eq!(
575             *commitish,
576             Commitish::Rev("1e09e50e8d15580b5929adbade9c782a6833e4a0".into())
577         );
578     }
579 
580     #[test]
detect_unused_annotation()581     fn detect_unused_annotation() {
582         // Create a config with some random annotation
583         let mut config = Config::default();
584         config.annotations.insert(
585             CrateNameAndVersionReq::new("mock-crate".to_owned(), "0.1.0".parse().unwrap()),
586             CrateAnnotations::default(),
587         );
588 
589         let result = Annotations::new(test::metadata::no_deps(), test::lockfile::no_deps(), config);
590         assert!(result.is_err());
591 
592         let result_str = format!("{result:?}");
593         assert!(result_str.contains("Unused annotations were provided. Please remove them"));
594         assert!(result_str.contains("mock-crate"));
595     }
596 
597     #[test]
defaults_from_package_metadata()598     fn defaults_from_package_metadata() {
599         let crate_id = CrateId::new(
600             "has_package_metadata".to_owned(),
601             semver::Version::new(0, 0, 0),
602         );
603         let crate_name_and_version_req = CrateNameAndVersionReq::new(
604             "has_package_metadata".to_owned(),
605             "0.0.0".parse().unwrap(),
606         );
607         let annotations = CrateAnnotations {
608             rustc_env: Some(Select::from_value(BTreeMap::from([(
609                 "BAR".to_owned(),
610                 "bar is set".to_owned(),
611             )]))),
612             ..CrateAnnotations::default()
613         };
614 
615         let mut config = Config::default();
616         config
617             .annotations
618             .insert(crate_name_and_version_req, annotations.clone());
619 
620         // Combine the above annotations with default values provided by the
621         // crate author in package metadata.
622         let combined_annotations = Annotations::new(
623             test::metadata::has_package_metadata(),
624             test::lockfile::has_package_metadata(),
625             config,
626         )
627         .unwrap();
628 
629         let extras = &combined_annotations.pairred_extras[&crate_id].crate_extra;
630         let expected = CrateAnnotations {
631             // This comes from has_package_metadata's [package.metadata.bazel].
632             additive_build_file_content: Some("genrule(**kwargs)\n".to_owned()),
633             // The package metadata defines a default rustc_env containing FOO,
634             // but it is superseded by a rustc_env annotation containing only
635             // BAR. These dictionaries are intentionally not merged together.
636             ..annotations
637         };
638         assert_eq!(*extras, expected);
639     }
640 
641     #[test]
test_find_workspace_metadata()642     fn test_find_workspace_metadata() {
643         let mut metadata = metadata::common();
644         metadata.workspace_metadata = json!({
645             "cargo-bazel": {
646             "package_prefixes": {},
647             "sources": {},
648             "tree_metadata": {
649                 "bitflags 1.3.2": {
650                     "common": {
651                         "features": [
652                             "default",
653                         ],
654                     },
655                     "selects": {
656                         "x86_64-unknown-linux-gnu": {
657                             "features": [
658                                 "std",
659                             ],
660                             "deps": [
661                                 "libc 1.2.3",
662                             ],
663                         },
664                     }
665                 }
666             },
667         }
668         });
669 
670         let mut select = Select::new();
671         select.insert(
672             CargoTreeEntry {
673                 features: BTreeSet::from(["default".to_owned()]),
674                 deps: BTreeSet::new(),
675             },
676             None,
677         );
678         select.insert(
679             CargoTreeEntry {
680                 features: BTreeSet::from(["std".to_owned()]),
681                 deps: BTreeSet::from([CrateId::new("libc".to_owned(), Version::new(1, 2, 3))]),
682             },
683             Some("x86_64-unknown-linux-gnu".to_owned()),
684         );
685         let expected = TreeResolverMetadata::from([(
686             CrateId::new("bitflags".to_owned(), Version::new(1, 3, 2)),
687             select,
688         )]);
689 
690         let result = find_workspace_metadata(&metadata).unwrap();
691 
692         assert_eq!(expected, result.tree_metadata);
693     }
694 }
695