1use crate::hack::Collect;
2use crate::metadata::{DepKindInfo, Link};
3use cargo_metadata::{Metadata, Package, PackageId, Source};
4use cargo_platform::Cfg;
5use dot::{GraphWalk, Labeller};
6use petgraph::graph::{EdgeIndex, NodeIndex};
7use petgraph::visit::{Dfs, EdgeFiltered, EdgeRef};
8use petgraph::Graph;
9use std::borrow::Cow;
10use std::collections::{BTreeMap, BTreeSet};
11use std::ops::Index;
12use tracing::{debug, error, info, trace};
13
14#[derive(Copy, Clone, Ord, PartialEq, Eq, PartialOrd, Debug)]
15pub enum Feature<'a> {
17 Root,
19 Workspace(Fid<'a>),
21 External(Fid<'a>),
23}
24
25impl std::fmt::Display for Feature<'_> {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 Feature::Root => f.write_str("root"),
29 Feature::Workspace(fid) | Feature::External(fid) => fid.fmt(f),
30 }
31 }
32}
33
34impl<'a> Feature<'a> {
35 #[must_use]
36 pub const fn fid(&self) -> Option<Fid<'a>> {
37 match self {
38 Feature::Root => None,
39 Feature::Workspace(fid) | Feature::External(fid) => Some(*fid),
40 }
41 }
42
43 #[must_use]
44 pub fn pid(&self) -> Option<Pid<'a>> {
45 self.fid().map(|fid| fid.pid)
46 }
47
48 #[must_use]
49 pub fn package_id(&self) -> Option<&PackageId> {
50 let Pid(pid, meta) = self.pid()?;
51 Some(&meta.packages[pid].id)
52 }
53
54 #[must_use]
55 pub const fn is_workspace(&self) -> bool {
56 match self {
57 Feature::Root | Feature::Workspace(_) => true,
58 Feature::External(_) => false,
59 }
60 }
61}
62
63pub struct FeatGraph<'a> {
64 pub root: NodeIndex,
66 pub workspace_members: BTreeSet<Pid<'a>>,
68 pub features: Graph<Feature<'a>, Link>,
71 fids: BTreeMap<Fid<'a>, NodeIndex>,
73 cache: BTreeMap<&'a PackageId, Pid<'a>>,
75
76 pub fid_cache: BTreeMap<Fid<'a>, NodeIndex>,
77
78 meta: &'a Metadata,
80
81 pub platforms: Vec<&'a str>,
82 pub cfgs: Vec<Cfg>,
83 pub triggers: Vec<Trigger<'a>>,
84
85 pub focus_nodes: Option<BTreeSet<NodeIndex>>,
86 pub focus_edges: Option<BTreeSet<EdgeIndex>>,
87 pub focus_targets: Option<BTreeSet<NodeIndex>>,
88}
89
90impl<'a> Index<Pid<'a>> for FeatGraph<'a> {
91 type Output = NodeIndex;
92
93 fn index(&self, index: Pid<'a>) -> &Self::Output {
94 &self.fid_cache[&index.root()]
95 }
96}
97
98impl<'a> Index<NodeIndex> for FeatGraph<'a> {
99 type Output = Fid<'a>;
100
101 fn index(&self, index: NodeIndex) -> &Self::Output {
102 match &self.features[index] {
103 Feature::Root => panic!("root node fid"),
104 Feature::Workspace(f) => f,
105 Feature::External(f) => f,
106 }
107 }
108}
109
110#[derive(Debug)]
111pub struct Trigger<'a> {
112 pub package: Pid<'a>, pub feature: Fid<'a>, pub weak_dep: Fid<'a>, pub weak_feat: Fid<'a>, }
127
128impl<'a> FeatGraph<'a> {
129 pub fn fid_index(&mut self, fid: Fid<'a>) -> NodeIndex {
130 *self.fids.entry(fid).or_insert_with(|| {
131 if self.workspace_members.contains(&fid.pid) {
132 self.features.add_node(Feature::Workspace(fid))
133 } else {
134 self.features.add_node(Feature::External(fid))
135 }
136 })
137 }
138
139 #[must_use]
141 pub fn base_node(&self, node: NodeIndex) -> Option<NodeIndex> {
142 self.fid_cache
143 .get(&self.features[node].fid()?.get_base())
144 .copied()
145 }
146
147 pub fn shrink_to_target(&mut self) -> anyhow::Result<()> {
148 info!("Shrinking to current target");
149 let g = EdgeFiltered::from_fn(&self.features, |e| {
150 e.weight().satisfies(
151 self.features[e.source()],
152 Collect::DevTarget,
153 &self.platforms,
154 &self.cfgs,
155 )
156 });
157 let mut dfs = Dfs::new(&g, self.root);
158 let mut this = BTreeSet::new();
159 while let Some(ix) = dfs.next(&g) {
160 this.insert(ix);
161 }
162
163 self.features.retain_nodes(|_, ix| this.contains(&ix));
164 self.rebuild_cache()?;
165
166 Ok(())
167 }
168
169 pub fn init(
170 meta: &'a Metadata,
171 platforms: Vec<&'a str>,
172 cfgs: Vec<Cfg>,
173 ) -> anyhow::Result<Self> {
174 if meta.resolve.is_none() {
175 anyhow::bail!("Cargo couldn't produce resolved dependencies")
176 }
177
178 let cache = meta
179 .packages
180 .iter()
181 .enumerate()
182 .map(|(ix, package)| (&package.id, Pid(ix, meta)))
183 .collect::<BTreeMap<_, _>>();
184
185 let mut features = Graph::new();
186 let root = features.add_node(Feature::Root);
187
188 let mut graph = Self {
189 workspace_members: meta
190 .workspace_members
191 .iter()
192 .filter_map(|pid| cache.get(pid))
193 .copied()
194 .collect::<BTreeSet<_>>(),
195 features,
196 root,
197 platforms,
198 fids: BTreeMap::new(),
199 triggers: Vec::new(),
200 fid_cache: BTreeMap::new(),
201 cache,
202 meta,
203 cfgs,
204 focus_nodes: None,
205 focus_edges: None,
206 focus_targets: None,
207 };
208
209 for (ix, package) in meta.packages.iter().enumerate() {
210 graph.add_package(ix, package, &meta.packages)?;
211 }
212
213 graph.rebuild_cache()?;
214
215 Ok(graph)
216 }
217
218 pub fn optimize(&mut self, no_transitive: bool) -> anyhow::Result<()> {
219 info!("Optimization pass: trim unused features");
220 self.trim_unused_features();
221
222 if !no_transitive {
223 info!("Optimization pass: transitive reduction");
224 self.transitive_reduction();
225 }
226
227 self.rebuild_cache()?;
228 Ok(())
229 }
230
231 pub fn rebuild_cache(&mut self) -> anyhow::Result<()> {
232 info!("Rebuilding feature id cache");
233 self.fids.clear();
234 self.fid_cache.clear();
235 for node in self.features.node_indices() {
236 if let Some(fid) = self.features[node].fid() {
237 self.fids.insert(fid, node);
238 }
239
240 if let Some(feat) = self.features[node].fid() {
241 self.fid_cache.insert(feat, node);
242 }
243 }
244 Ok(())
245 }
246
247 fn transitive_reduction(&mut self) {
248 use petgraph::algo::tred::dag_to_toposorted_adjacency_list;
249 let graph = &mut self.features;
250 let before = graph.edge_count();
251 let toposort = match petgraph::algo::toposort(&*graph, None) {
252 Ok(t) => t,
253 Err(err) => {
254 error!("Cyclic dependencies are detected {err:?}, skipping transitive reduction");
255 return;
256 }
257 };
258
259 let (adj_list, revmap) =
260 dag_to_toposorted_adjacency_list::<_, NodeIndex>(&*graph, &toposort);
261 let (reduction, _closure) =
262 petgraph::algo::tred::dag_transitive_reduction_closure(&adj_list);
263
264 graph.retain_edges(|x, y| {
265 if let Some((f, t)) = x.edge_endpoints(y) {
266 reduction.contains_edge(revmap[f.index()], revmap[t.index()])
267 } else {
268 false
269 }
270 });
271 let after = graph.edge_count();
272 debug!("Transitive reduction, edges {before} -> {after}");
273 }
274
275 fn trim_unused_features(&mut self) {
279 let mut to_remove = Vec::new();
280 loop {
281 for f in self.features.externals(petgraph::EdgeDirection::Incoming) {
282 if let Feature::External(..) = self.features[f] {
283 to_remove.push(f);
284 }
285 }
286 if to_remove.is_empty() {
287 break;
288 }
289 for f in to_remove.drain(..) {
290 self.features.remove_node(f);
291 }
292 }
293 }
294
295 fn add_package(
296 &mut self,
297 ix: usize,
298 package: &'a Package,
299 packages: &'a [Package],
300 ) -> anyhow::Result<()> {
301 debug!("== adding package {}", package.id);
302 let this = Pid(ix, self.meta);
303 let base_ix = self.fid_index(this.base());
304
305 let workspace_member = self.workspace_members.contains(&this);
306
307 if workspace_member {
309 self.add_edge(self.root, this, false, DepKindInfo::NORMAL)?;
310 }
311
312 let mut deps = BTreeMap::new();
314 for dep in &package.dependencies {
315 if !workspace_member && dep.kind == cargo_metadata::DependencyKind::Development {
316 trace!("Skipping external dev dependency {dep:?}");
317 continue;
318 }
319
320 let source_matches = |a: Option<&Source>, b: Option<&String>| match (a, b) {
321 (None, None) => true,
322 (Some(a), Some(b)) => {
323 if &a.repr == b || (a.repr.starts_with("git") && a.repr.starts_with(b)) {
324 true
325 } else {
326 trace!("ignoring a candidate {package:?} for {dep:?} due to source mismatch: {a:?} != {b:?}");
327 false
328 }
329 }
330 _ => false,
331 };
332 let resolved = match packages.iter().find(|p| {
334 p.name == dep.name
335 && dep.req.matches(&p.version)
336 && source_matches(p.source.as_ref(), dep.source.as_ref())
337 }) {
338 Some(res) => res,
339 None => {
340 debug!(
341 "cargo metadta did not include optional dependency \"{} {}\" \
342 requested by \"{} {}\", skipping",
343 dep.name, dep.req, package.name, package.version
344 );
345 continue;
346 }
347 };
348
349 let this = if dep.optional {
354 match dep.rename.as_ref() {
355 Some(name) => this.named(name).get_index(self)?,
356 None => this.named(&dep.name).get_index(self)?,
357 }
358 } else {
359 base_ix
360 };
361
362 let remote = if dep.uses_default_features {
365 Some(self.add_edge(this, resolved, false, dep.into())?)
366 } else if let Some(pid) = self.cache.get(&resolved.id) {
367 let fid = pid.base();
368 Some(self.add_edge(this, fid, false, dep.into())?)
369 } else {
370 None
371 };
372 for feat in &dep.features {
374 self.add_edge(this, (resolved, feat.as_str()), false, dep.into())?;
375 }
376
377 if let Some(remote) = remote {
379 let name = dep.rename.clone().unwrap_or_else(|| resolved.name.clone());
380 deps.insert(name, (resolved, dep, remote));
381 }
382 }
383
384 for (this_feat, feat_deps) in &package.features {
385 let feat_ix = self.fid_index(this.named(this_feat));
386 self.add_edge(feat_ix, base_ix, false, DepKindInfo::NORMAL)?;
387
388 for feat_dep in feat_deps.iter() {
389 match FeatTarget::from(feat_dep.as_str()) {
390 FeatTarget::Named { name } => {
391 self.add_edge(feat_ix, this.named(name), false, DepKindInfo::NORMAL)?;
392 }
393 FeatTarget::Dependency { krate } => {
394 if let Some(&(_dep, link, remote)) = deps.get(krate) {
395 self.add_edge(feat_ix, remote, true, link.into())?;
396 } else {
397 debug!("skipping disabled optional dependency {krate}");
398 }
399 }
400 FeatTarget::Remote { krate, feat } => {
401 if let Some(&(dep, link, _remote)) = deps.get(krate) {
402 self.add_edge(feat_ix, (dep, feat), true, link.into())?;
403 } else {
404 debug!("skipping disabled optional dependency {krate}");
405 }
406 }
407 FeatTarget::Cond { krate, feat } => {
408 if let Some(dep) = deps
409 .get(krate)
410 .and_then(|&(dep, _link, _remote)| self.cache.get(&dep.id).copied())
411 {
412 let trigger = Trigger {
413 package: this,
414 feature: this.named(this_feat),
415 weak_dep: this.named(krate),
416 weak_feat: dep.named(feat),
417 };
418 self.triggers.push(trigger);
419 } else {
420 debug!("skipping disabled optional dependency {krate}");
421 }
422 }
423 }
424 }
425 }
426
427 Ok(())
428 }
429
430 pub fn add_edge<A, B>(
431 &mut self,
432 a: A,
433 b: B,
434 optional: bool,
435 kind: DepKindInfo,
436 ) -> anyhow::Result<NodeIndex>
437 where
438 A: HasIndex<'a>,
439 B: HasIndex<'a>,
440 {
441 let a = a.get_index(self)?;
442 let b = b.get_index(self)?;
443 trace!(
444 "adding {}edge {a:?} -> {b:?}: {kind:?}\n\t{:?}\n\t{:?}",
445 if optional { "optional " } else { "" },
446 self.features[a],
447 self.features[b]
448 );
449
450 if let Some(index) = self.features.find_edge(a, b) {
451 let old_link = &mut self.features[index];
452 if !old_link.kinds.contains(&kind) {
453 old_link.kinds.push(kind);
454 }
455 old_link.optional &= optional;
456 } else {
457 let link = Link {
458 optional,
459 kinds: vec![kind],
460 };
461 self.features.add_edge(a, b, link);
462 }
463 Ok(b)
464 }
465}
466
467#[derive(Copy, Clone)]
468pub struct Pid<'a>(usize, &'a Metadata);
469
470impl<'a> Pid<'a> {
471 #[must_use]
472 pub fn package(self) -> &'a cargo_metadata::Package {
473 &self.1.packages[self.0]
474 }
475}
476
477impl<'a> Pid<'a> {
478 #[must_use]
479 pub fn root(self) -> Fid<'a> {
480 if self.package().features.contains_key("default") {
481 self.named("default")
482 } else {
483 self.base()
484 }
485 }
486
487 #[must_use]
488 pub const fn base(self) -> Fid<'a> {
489 Fid {
490 pid: self,
491 dep: Feat::Base,
492 }
493 }
494 #[must_use]
495 pub const fn named(self, name: &'a str) -> Fid<'a> {
496 Fid {
497 pid: self,
498 dep: Feat::Named(name),
499 }
500 }
501}
502
503impl<'a> PartialEq for Pid<'a> {
504 fn eq(&self, other: &Self) -> bool {
505 self.0 == other.0
506 }
507}
508
509impl<'a> Eq for Pid<'a> {}
510
511impl<'a> PartialOrd for Pid<'a> {
512 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
513 Some(self.cmp(other))
514 }
515}
516
517impl<'a> Ord for Pid<'a> {
518 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
519 self.0.cmp(&other.0)
520 }
521}
522
523impl std::fmt::Debug for Pid<'_> {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 let meta = &self.1.packages[self.0];
526 write!(f, "Pid({} / {})", self.0, meta.id)
527 }
528}
529
530#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
531pub struct Fid<'a> {
532 pub pid: Pid<'a>,
534 pub dep: Feat<'a>,
535}
536
537impl std::fmt::Display for Fid<'_> {
538 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539 let id = &self.pid.package().id;
540 match self.dep {
541 Feat::Base => write!(f, "{id}"),
542 Feat::Named(name) => write!(f, "{id}:{name}"),
543 }
544 }
545}
546
547impl std::fmt::Display for Feat<'_> {
548 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549 match self {
550 Feat::Base => f.write_str(":base:"),
551 Feat::Named(name) => f.write_str(name),
552 }
553 }
554}
555
556#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
557pub enum Feat<'a> {
558 Base,
560 Named(&'a str),
562}
563
564impl<'a> GraphWalk<'a, NodeIndex, EdgeIndex> for FeatGraph<'a> {
565 fn nodes(&'a self) -> dot::Nodes<'a, NodeIndex> {
566 Cow::from(match &self.focus_nodes {
567 Some(f) => f.iter().copied().collect::<Vec<_>>(),
568 None => self.features.node_indices().collect::<Vec<_>>(),
569 })
570 }
571
572 fn edges(&'a self) -> dot::Edges<'a, EdgeIndex> {
573 Cow::from(match &self.focus_edges {
574 Some(f) => f.iter().copied().collect::<Vec<_>>(),
575 None => self.features.edge_indices().collect::<Vec<_>>(),
576 })
577 }
578
579 fn source(&'a self, edge: &EdgeIndex) -> NodeIndex {
580 self.features.edge_endpoints(*edge).unwrap().0
581 }
582
583 fn target(&'a self, edge: &EdgeIndex) -> NodeIndex {
584 self.features.edge_endpoints(*edge).unwrap().1
585 }
586}
587
588impl<'a> Labeller<'a, NodeIndex, EdgeIndex> for FeatGraph<'a> {
589 fn graph_id(&'a self) -> dot::Id<'a> {
590 dot::Id::new("graphname").unwrap()
591 }
592
593 fn node_id(&'a self, n: &NodeIndex) -> dot::Id<'a> {
594 dot::Id::new(format!("n{}", n.index())).unwrap()
595 }
596
597 fn node_shape(&'a self, node: &NodeIndex) -> Option<dot::LabelText<'a>> {
598 let fid = self.features[*node].fid()?;
599 match fid.dep {
600 Feat::Base => Some(dot::LabelText::label("octagon")),
601 Feat::Named(_) => None,
602 }
603 }
604
605 fn node_label(&'a self, n: &NodeIndex) -> dot::LabelText<'a> {
606 let mut fmt = String::new();
607 match self.features[*n].fid() {
608 Some(fid) => {
609 let package = fid.pid.package();
610 fmt.push_str(&package.name);
611
612 if let Some(src) = package.source.as_ref() {
613 if src.repr.starts_with("git") {
614 fmt.push_str(" git");
615 } else {
616 fmt.push_str(&format!(" {}", package.version));
617 }
618 }
619 match fid.dep {
620 Feat::Base => {}
621 Feat::Named(name) => {
622 fmt.push('\n');
623 fmt.push_str(name);
624 }
625 }
626
627 dot::LabelText::LabelStr(fmt.into())
628 }
629 None => dot::LabelText::LabelStr("root".into()),
630 }
631 }
632
633 fn edge_label(&'a self, e: &EdgeIndex) -> dot::LabelText<'a> {
634 let _ = e;
635 dot::LabelText::LabelStr("".into())
636 }
637
638 fn node_style(&'a self, n: &NodeIndex) -> dot::Style {
639 if let Some(fid) = self.features[*n].fid() {
640 if self.workspace_members.contains(&fid.pid) {
641 dot::Style::None
642 } else {
643 dot::Style::Filled
644 }
645 } else {
646 dot::Style::None
647 }
648 }
649
650 fn node_color(&'a self, node: &NodeIndex) -> Option<dot::LabelText<'a>> {
651 self.focus_targets
652 .as_ref()?
653 .contains(node)
654 .then(|| dot::LabelText::LabelStr("pink".into()))
655 }
656
657 fn edge_end_arrow(&'a self, _e: &EdgeIndex) -> dot::Arrow {
658 dot::Arrow::default()
659 }
660
661 fn edge_start_arrow(&'a self, _e: &EdgeIndex) -> dot::Arrow {
662 dot::Arrow::default()
663 }
664
665 fn edge_style(&'a self, e: &EdgeIndex) -> dot::Style {
666 if self.features[*e].is_dev_only() {
667 dot::Style::Dashed
668 } else {
669 dot::Style::None
670 }
671 }
672
673 fn edge_color(&'a self, e: &EdgeIndex) -> Option<dot::LabelText<'a>> {
674 if self.features[*e].optional {
675 Some(dot::LabelText::label("grey"))
676 } else {
677 Some(dot::LabelText::label("black"))
678 }
679 }
680
681 fn kind(&self) -> dot::Kind {
682 dot::Kind::Digraph
683 }
684}
685
686pub trait HasIndex<'a> {
687 fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex>;
688}
689
690impl HasIndex<'_> for NodeIndex {
691 fn get_index(self, _graph: &mut FeatGraph) -> anyhow::Result<NodeIndex> {
692 Ok(self)
693 }
694}
695
696impl<'a> HasIndex<'a> for Fid<'a> {
697 fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex> {
698 Ok(graph.fid_index(self))
699 }
700}
701
702impl<'a> HasIndex<'a> for Pid<'a> {
703 fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex> {
704 if self.package().features.contains_key("default") {
705 Ok(graph.fid_index(self.named("default")))
706 } else {
707 Ok(graph.fid_index(self.base()))
708 }
709 }
710}
711
712impl<'a> HasIndex<'a> for &'a Package {
713 fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex> {
714 (*graph
715 .cache
716 .get(&self.id)
717 .ok_or_else(|| anyhow::anyhow!("No cached value for {:?}", self.id))?)
718 .get_index(graph)
719 }
720}
721
722impl<'a> HasIndex<'a> for (&'a Package, &'a str) {
723 fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex> {
724 let package_id = &self.0.id;
725 let feat = self.1;
726 let pid = *graph
727 .cache
728 .get(package_id)
729 .ok_or_else(|| anyhow::anyhow!("No cached value for {package_id:?}"))?;
730 pid.named(feat).get_index(graph)
731 }
732}
733
734#[derive(Copy, Clone, Debug, Eq, PartialEq)]
735pub enum FeatTarget<'a> {
736 Named { name: &'a str },
737 Dependency { krate: &'a str },
738 Remote { krate: &'a str, feat: &'a str },
739 Cond { krate: &'a str, feat: &'a str },
740}
741
742impl<'a> From<&'a str> for FeatTarget<'a> {
743 fn from(s: &'a str) -> Self {
744 if let Some(krate) = s.strip_prefix("dep:") {
745 FeatTarget::Dependency { krate }
746 } else if let Some((krate, feat)) = s.split_once("?/") {
747 FeatTarget::Cond { krate, feat }
748 } else if let Some((krate, feat)) = s.split_once('/') {
749 FeatTarget::Remote { krate, feat }
750 } else {
751 FeatTarget::Named { name: s }
752 }
753 }
754}
755
756impl Fid<'_> {
757 #[must_use]
758 pub const fn get_base(&self) -> Self {
760 Self {
761 dep: Feat::Base,
762 ..*self
763 }
764 }
765}
766
767#[cfg(test)]
768mod test {
769 use super::*;
770 #[test]
771 fn feat_target() {
772 use FeatTarget::*;
773 assert_eq!(FeatTarget::from("quote"), Named { name: "quote" });
774 assert_eq!(
775 FeatTarget::from("dep:serde_json"),
776 Dependency {
777 krate: "serde_json"
778 }
779 );
780 assert_eq!(
781 FeatTarget::from("syn/extra-tr"),
782 Remote {
783 krate: "syn",
784 feat: "extra-tr"
785 }
786 );
787 assert_eq!(
788 FeatTarget::from("rgb?/serde"),
789 Cond {
790 krate: "rgb",
791 feat: "serde"
792 }
793 );
794 }
795
796 fn get_demo_meta(ix: usize) -> anyhow::Result<Metadata> {
797 let path = format!(
798 "{}/test_workspaces/{ix}/metadata.json",
799 env!("CARGO_MANIFEST_DIR")
800 );
801 let data = std::fs::read_to_string(path)?;
802 Ok(cargo_metadata::MetadataCommand::parse(data)?)
803 }
804
805 fn process_fg_with<F>(ix: usize, op: F) -> anyhow::Result<()>
806 where
807 F: FnOnce(&mut FeatGraph) -> anyhow::Result<()>,
808 {
809 let meta = get_demo_meta(ix)?;
810 let platform = target_spec::Platform::current()?;
811 let triplets = vec![platform.triple_str()];
812 let mut fg = FeatGraph::init(&meta, triplets, Vec::new())?;
813 op(&mut fg)
814 }
815
816 #[test]
817 fn metadata_snapshot_2() -> anyhow::Result<()> {
818 process_fg_with(2, |_| Ok(()))?;
819 Ok(())
820 }
821
822 #[test]
823 fn metadata_snapshot_3() -> anyhow::Result<()> {
824 process_fg_with(3, |_| Ok(()))?;
825 Ok(())
826 }
827
828 #[test]
829 fn metadata_snapshot_4() -> anyhow::Result<()> {
830 process_fg_with(4, |_| Ok(()))?;
831 Ok(())
832 }
833
834 #[test]
835 fn metadata_snapshot_5() -> anyhow::Result<()> {
836 process_fg_with(5, |_fg| {
837 Ok(())
840 })
841 }
842}