workspacer_detect_circular_deps/
dependency_tree.rs1crate::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 async fn generate_dependency_tree(&self) -> Result<WorkspaceDependencyGraph, WorkspaceError> {
25 let metadata = self.get_cargo_metadata().await?;
27
28 let mut graph: WorkspaceDependencyGraph = DiGraph::new();
30
31 let mut id_to_node: HashMap<PackageId, NodeIndex> = HashMap::new();
33
34 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 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 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 #[tokio::test]
72 async fn test_single_crate_no_deps() {
73 let single_crate = CrateConfig::new("single_crate").with_src_files();
75 let root_path = create_mock_workspace(vec![single_crate])
77 .await
78 .expect("Failed to create mock workspace");
79
80 let workspace = Workspace::<PathBuf,CrateHandle>::new(&root_path)
83 .await
84 .expect("Failed to create Workspace from mock dir");
85
86 let dep_graph = workspace.generate_dependency_tree().await
88 .expect("generate_dependency_tree should succeed");
89
90 assert_eq!(dep_graph.node_count(), 1, "One crate => one node");
92 assert_eq!(dep_graph.edge_count(), 0, "No edges => no deps");
93 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 #[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 assert_eq!(dep_graph.node_count(), 2);
117 assert_eq!(dep_graph.edge_count(), 0);
118
119 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 #[tokio::test]
136 async fn test_simple_dependency_one_edge() {
137 let crate_a = CrateConfig::new("crate_a").with_src_files();
143 let crate_b = CrateConfig::new("crate_b").with_src_files();
144
145 let root_path = create_mock_workspace(vec![crate_a, crate_b])
147 .await
148 .expect("create mock workspace");
149
150 let crate_a_path = root_path.join("crate_a");
152 let cargo_toml_a = crate_a_path.join("Cargo.toml");
153
154 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 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 assert_eq!(dep_graph.node_count(), 2, "two crates");
176 assert_eq!(dep_graph.edge_count(), 1, "one dependency edge");
177
178 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 assert_eq!(src_name, "crate_a");
188 assert_eq!(dst_name, "crate_b");
189 }
190
191 #[tokio::test]
195 async fn test_multiple_dependencies() {
196 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 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 {
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 let workspace = Workspace::<PathBuf,CrateHandle>::new(&root_path).await.unwrap();
241 let dep_graph = workspace.generate_dependency_tree().await.unwrap();
242
243 assert_eq!(dep_graph.node_count(), 3);
245 assert_eq!(dep_graph.edge_count(), 3);
247
248 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 #[tokio::test]
270 async fn test_generate_dependency_tree_dot() {
271 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 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 let workspace = Workspace::<PathBuf,CrateHandle>::new(&root_path).await.unwrap();
291 let dot_str = workspace.generate_dependency_tree_dot().await.unwrap();
293
294 assert!(dot_str.contains("crate_a"), "DOT should mention crate_a");
297 assert!(dot_str.contains("crate_b"), "DOT should mention crate_b");
298 assert!(dot_str.contains("->"), "Should have an edge in DOT");
303 }
304
305 #[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 assert!(dot.contains("crate_x"));
320 assert!(dot.contains("crate_y"));
321 assert!(!dot.contains("->"), "No edges => no arrow in the DOT");
323 }
324
325 #[tokio::test]
330 async fn test_circular_dependency() {
331 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 let workspace = Workspace::<PathBuf,CrateHandle>::new(&root).await.unwrap();
342 let result = workspace.generate_dependency_tree().await;
343 match result {
344 Ok(graph) => {
345 println!("Graph had {} nodes and {} edges", graph.node_count(), graph.edge_count());
348 }
349 Err(e) => {
350 println!("We got an error, possibly due to cycle: {:?}", e);
352 }
353 }
354 }
357}