workspacer_detect_circular_deps/
dependency_tree.rs

1// ---------------- [ File: workspacer-detect-circular-deps/src/dependency_tree.rs ]
2crate::ix!();
3
4#[async_trait]
5pub trait GenerateDependencyTree {
6
7    type Tree;
8    type Error;
9
10    async fn generate_dependency_tree(&self) -> Result<Self::Tree, Self::Error>;
11    async fn generate_dependency_tree_dot(&self) -> Result<String, Self::Error>;
12}
13
14pub type WorkspaceDependencyGraph = DiGraph<String, ()>;
15
16#[async_trait]
17impl<P,H:CrateHandleInterface<P>> GenerateDependencyTree for Workspace<P,H> 
18where for<'async_trait> P: From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait
19{
20    type Tree = WorkspaceDependencyGraph;
21    type Error = WorkspaceError;
22
23    /// Generates a dependency tree for all crates in the workspace.
24    async fn generate_dependency_tree(&self) -> Result<WorkspaceDependencyGraph, WorkspaceError> {
25        // Use cargo_metadata to get the metadata
26        let metadata = self.get_cargo_metadata().await?;
27
28        // Create a directed graph
29        let mut graph: WorkspaceDependencyGraph = DiGraph::new();
30
31        // Map package IDs to package names and their corresponding node index in the graph
32        let mut id_to_node: HashMap<PackageId, NodeIndex> = HashMap::new();
33
34        // Add nodes to the graph
35        for package in metadata.packages {
36            let node = graph.add_node(package.name.clone());
37            id_to_node.insert(package.id.clone(), node);
38        }
39
40        // Add edges (dependencies) to the graph
41        if let Some(resolve) = &metadata.resolve {
42            for node in &resolve.nodes {
43                let package_node = id_to_node[&node.id];
44
45                for dep in &node.deps {
46                    if let Some(dep_node) = id_to_node.get(&dep.pkg) {
47                        graph.add_edge(package_node, *dep_node, ());
48                    }
49                }
50            }
51        }
52
53        info!("dependency tree: {:#?}", graph);
54
55        Ok(graph)
56    }
57
58    /// Generates the dependency tree and returns it in DOT format.
59    async fn generate_dependency_tree_dot(&self) -> Result<String, WorkspaceError> {
60        let graph = self.generate_dependency_tree().await?;
61        let dot = Dot::with_config(&graph, &[DotConfig::EdgeNoLabel]);
62        Ok(format!("{:?}", dot))
63    }
64}
65
66#[cfg(test)]
67mod test_generate_dependency_tree {
68    use super::*;
69
70    /// 1) A workspace with a single crate, no dependencies => one node, no edges.
71    #[tokio::test]
72    async fn test_single_crate_no_deps() {
73        // Let's define exactly one crate config
74        let single_crate = CrateConfig::new("single_crate").with_src_files(); 
75        // Create the mock workspace
76        let root_path = create_mock_workspace(vec![single_crate])
77            .await
78            .expect("Failed to create mock workspace");
79
80        // Convert to a Workspace. 
81        // We'll do something like:
82        let workspace = Workspace::<PathBuf,CrateHandle>::new(&root_path)
83            .await
84            .expect("Failed to create Workspace from mock dir");
85
86        // Now generate the dependency tree
87        let dep_graph = workspace.generate_dependency_tree().await
88            .expect("generate_dependency_tree should succeed");
89
90        // We expect exactly 1 node, named "single_crate", no edges
91        assert_eq!(dep_graph.node_count(), 1, "One crate => one node");
92        assert_eq!(dep_graph.edge_count(), 0, "No edges => no deps");
93        // For extra certainty, we can fetch the node name:
94        let node_idx = dep_graph.node_indices().next().unwrap();
95        let node_name = &dep_graph[node_idx];
96        assert_eq!(node_name, "single_crate");
97    }
98
99    /// 2) Multiple crates, no cross-dependencies => multiple nodes, zero edges.
100    #[tokio::test]
101    async fn test_multiple_crates_no_cross_deps() {
102        let crate_a = CrateConfig::new("crate_a").with_src_files();
103        let crate_b = CrateConfig::new("crate_b").with_src_files();
104        let root_path = create_mock_workspace(vec![crate_a, crate_b])
105            .await
106            .expect("Failed to create mock workspace with multiple crates");
107
108        let workspace = Workspace::<PathBuf,CrateHandle>::new(&root_path)
109            .await
110            .expect("Failed to create workspace");
111
112        let dep_graph = workspace.generate_dependency_tree().await
113            .expect("Should succeed");
114
115        // We have 2 crates, no dependencies => 2 nodes, 0 edges
116        assert_eq!(dep_graph.node_count(), 2);
117        assert_eq!(dep_graph.edge_count(), 0);
118
119        // Optionally verify the crate names in the graph
120        let mut names = Vec::new();
121        for idx in dep_graph.node_indices() {
122            names.push(dep_graph[idx].clone());
123        }
124        names.sort();
125        assert_eq!(names, vec!["crate_a".to_string(), "crate_b".to_string()]);
126    }
127
128    /// 3) One crate depends on another => we expect one edge in the graph.
129    ///
130    /// We'll demonstrate how to simulate that dependency:
131    ///   - crateB (lib)
132    ///   - crateA depends on crateB via local path:
133    ///       [dependencies]
134    ///       crate_b = { path = "../crate_b" }
135    #[tokio::test]
136    async fn test_simple_dependency_one_edge() {
137        // We'll define two crate configs, but we also need to manually add a local path dependency 
138        // to crate_a's Cargo.toml. We'll do that by adjusting the mock AFTER create_mock_workspace,
139        // or by passing a custom function that modifies the cargo toml content. 
140        // For brevity, let's do a "post-creation" step.
141
142        let crate_a = CrateConfig::new("crate_a").with_src_files();
143        let crate_b = CrateConfig::new("crate_b").with_src_files();
144
145        // Step 1: Create the workspace with these two crates
146        let root_path = create_mock_workspace(vec![crate_a, crate_b])
147            .await
148            .expect("create mock workspace");
149
150        // Step 2: Insert `[dependencies] crate_b = { path = "../crate_b" }` into crate_a's Cargo.toml.
151        let crate_a_path = root_path.join("crate_a");
152        let cargo_toml_a  = crate_a_path.join("Cargo.toml");
153
154        // We'll read the existing Cargo.toml content, append the dependency, rewrite
155        let content = tokio::fs::read_to_string(&cargo_toml_a).await
156            .expect("read cargo toml for crate_a");
157        let new_content = format!(
158            r#"{}
159[dependencies]
160crate_b = {{ path = "../crate_b" }}
161"#,
162            content
163        );
164        tokio::fs::write(&cargo_toml_a, new_content)
165            .await
166            .expect("rewrite cargo toml for crate_a with dependency on crate_b");
167
168        // Step 3: Construct the workspace & generate dependency graph
169        let workspace = Workspace::<PathBuf,CrateHandle>::new(&root_path).await.expect("create workspace");
170        let dep_graph = workspace.generate_dependency_tree().await
171            .expect("dep tree should succeed");
172
173        // We expect 2 nodes: crate_a, crate_b
174        // We expect 1 edge: from crate_a -> crate_b
175        assert_eq!(dep_graph.node_count(), 2, "two crates");
176        assert_eq!(dep_graph.edge_count(), 1, "one dependency edge");
177
178        // Check which edge specifically. Let's gather the edges:
179        let edges: Vec<_> = dep_graph.edge_references().collect();
180        let edge = edges[0];
181        let src_idx = edge.source();
182        let dst_idx = edge.target();
183        let src_name = &dep_graph[src_idx];
184        let dst_name = &dep_graph[dst_idx];
185
186        // We expect src_name = "crate_a", dst_name = "crate_b"
187        assert_eq!(src_name, "crate_a");
188        assert_eq!(dst_name, "crate_b");
189    }
190
191    /// 4) If we have multiple dependencies (A depends on B, A depends on C, B depends on C, etc.),
192    ///    we can do a more advanced scenario. 
193    ///    We'll just do one example. The principle is the same: we add local path deps, check edges.
194    #[tokio::test]
195    async fn test_multiple_dependencies() {
196        // Suppose we have crate_a, crate_b, crate_c, with:
197        //   crate_a depends on crate_b + crate_c
198        //   crate_b depends on crate_c
199        // So the graph has edges: A->B, A->C, B->C, C->(none)
200        let crate_a = CrateConfig::new("crate_a").with_src_files();
201        let crate_b = CrateConfig::new("crate_b").with_src_files();
202        let crate_c = CrateConfig::new("crate_c").with_src_files();
203
204        let root_path = create_mock_workspace(vec![crate_a, crate_b, crate_c])
205            .await
206            .expect("mock workspace creation");
207
208        // We'll define dependencies by editing Cargo.toml after creation:
209        // crate_a -> crate_b & crate_c
210        // crate_b -> crate_c
211        let a_toml = root_path.join("crate_a").join("Cargo.toml");
212        let b_toml = root_path.join("crate_b").join("Cargo.toml");
213
214        // Append [dependencies] lines:
215        {
216            let orig = tokio::fs::read_to_string(&a_toml).await.unwrap();
217            let new = format!(
218                r#"{}
219[dependencies]
220crate_b = {{ path = "../crate_b" }}
221crate_c = {{ path = "../crate_c" }}
222"#,
223                orig
224            );
225            tokio::fs::write(&a_toml, new).await.unwrap();
226        }
227        {
228            let orig = tokio::fs::read_to_string(&b_toml).await.unwrap();
229            let new = format!(
230                r#"{}
231[dependencies]
232crate_c = {{ path = "../crate_c" }}
233"#,
234                orig
235            );
236            tokio::fs::write(&b_toml, new).await.unwrap();
237        }
238
239        // Now create workspace & call generate_dependency_tree
240        let workspace = Workspace::<PathBuf,CrateHandle>::new(&root_path).await.unwrap();
241        let dep_graph = workspace.generate_dependency_tree().await.unwrap();
242
243        // We expect 3 nodes
244        assert_eq!(dep_graph.node_count(), 3);
245        // We expect 3 edges: A->B, A->C, B->C
246        assert_eq!(dep_graph.edge_count(), 3);
247
248        // We can confirm them specifically by checking each edge
249        // We'll do a quick approach:
250        let mut found_edges = Vec::new();
251        for edge in dep_graph.edge_references() {
252            let src_idx = edge.source();
253            let dst_idx = edge.target();
254            let src_name = &dep_graph[src_idx];
255            let dst_name = &dep_graph[dst_idx];
256            found_edges.push((src_name.clone(), dst_name.clone()));
257        }
258        found_edges.sort();
259        let expected = vec![
260            ("crate_a".to_string(), "crate_b".to_string()),
261            ("crate_a".to_string(), "crate_c".to_string()),
262            ("crate_b".to_string(), "crate_c".to_string()),
263        ];
264        assert_eq!(found_edges, expected);
265    }
266
267    /// 5) Now we test `generate_dependency_tree_dot()`, verifying we get a DOT representation
268    ///    with the correct node labels (the crate names). We won't parse the DOT deeply, but do partial checks.
269    #[tokio::test]
270    async fn test_generate_dependency_tree_dot() {
271        // We'll reuse the simple scenario: crate_a depends on crate_b
272        let crate_a = CrateConfig::new("crate_a").with_src_files();
273        let crate_b = CrateConfig::new("crate_b").with_src_files();
274        let root_path = create_mock_workspace(vec![crate_a, crate_b])
275            .await
276            .expect("mock workspace creation");
277        // Insert dependency a->b
278        let a_toml = root_path.join("crate_a").join("Cargo.toml");
279        let orig = tokio::fs::read_to_string(&a_toml).await.unwrap();
280        let new = format!(
281            r#"{}
282[dependencies]
283crate_b = {{ path = "../crate_b" }}
284"#,
285            orig
286        );
287        tokio::fs::write(&a_toml, new).await.unwrap();
288
289        // Build workspace
290        let workspace = Workspace::<PathBuf,CrateHandle>::new(&root_path).await.unwrap();
291        // Call generate_dependency_tree_dot
292        let dot_str = workspace.generate_dependency_tree_dot().await.unwrap();
293
294        // We'll do partial checks: confirm "crate_a" and "crate_b" appear
295        // in the DOT, and there's an edge a->b. 
296        assert!(dot_str.contains("crate_a"), "DOT should mention crate_a");
297        assert!(dot_str.contains("crate_b"), "DOT should mention crate_b");
298        // The exact DOT format might look like:
299        //   "digraph {\n    0 [label=\"crate_a\"]\n    1 [label=\"crate_b\"]\n    0 -> 1\n}"
300        // So let's just confirm it has something like -> 
301        // (the node numbering might be 0 or 1 in an arbitrary order)
302        assert!(dot_str.contains("->"), "Should have an edge in DOT");
303    }
304
305    /// 6) If the workspace has no `[dependencies]` or multiple crates, 
306    ///    `generate_dependency_tree_dot()` is just multiple nodes, zero edges. 
307    ///    That scenario is basically covered by test_multiple_crates_no_cross_deps, 
308    ///    but we can do a quick partial test if we like.
309    #[tokio::test]
310    async fn test_dot_no_edges() {
311        let crate_x = CrateConfig::new("crate_x").with_src_files();
312        let crate_y = CrateConfig::new("crate_y").with_src_files();
313        let root = create_mock_workspace(vec![crate_x, crate_y])
314            .await
315            .expect("mock ws creation");
316        let ws = Workspace::<PathBuf,CrateHandle>::new(&root).await.unwrap();
317        let dot = ws.generate_dependency_tree_dot().await.unwrap();
318        // Should contain crate_x, crate_y, but no "->"
319        assert!(dot.contains("crate_x"));
320        assert!(dot.contains("crate_y"));
321        // For zero edges, we presumably won't see any "->"
322        assert!(!dot.contains("->"), "No edges => no arrow in the DOT");
323    }
324
325    /// 7) If there's a cyclical or impossible dependency scenario, 
326    ///    cargo_metadata might fail or the graph might have cycles. 
327    ///    In your code you might return an error. We'll do a partial demonstration.
328    ///    It's tricky to set up a local cyc. We'll skip the details, but for completeness:
329    #[tokio::test]
330    async fn test_circular_dependency() {
331        // We'll try to set crate_a depends on crate_b, crate_b depends on crate_a
332        // This might cause cargo_metadata to fail with a cycle error, or might produce a partial result.
333        let crate_a = CrateConfig::new("crate_a").with_src_files();
334        let crate_b = CrateConfig::new("crate_b").with_src_files();
335        let root = create_mock_workspace(vec![crate_a, crate_b])
336            .await
337            .unwrap();
338        // add cross dependencies in both cargo tomls...
339        // ...
340        // Then:
341        let workspace = Workspace::<PathBuf,CrateHandle>::new(&root).await.unwrap();
342        let result = workspace.generate_dependency_tree().await;
343        match result {
344            Ok(graph) => {
345                // Possibly cargo_metadata is tolerant or partial. We can see if it forms a cycle
346                // You can test your code's logic for cycle detection if it’s in `detect_circular_dependencies()`.
347                println!("Graph had {} nodes and {} edges", graph.node_count(), graph.edge_count());
348            }
349            Err(e) => {
350                // Possibly you get a CargoMetadataError or a cycle error. 
351                println!("We got an error, possibly due to cycle: {:?}", e);
352            }
353        }
354        // There's no single universal outcome, as cargo might bail out or produce partial. 
355        // So adapt to your real code's behavior.
356    }
357}