workspacer_tree/
build_workspace_subtree.rs

1// ---------------- [ File: workspacer-tree/src/build_workspace_subtree.rs ]
2crate::ix!();
3
4#[async_trait]
5pub trait BuildWorkspaceSubTree {
6    async fn build_workspace_subtree(
7        &self,
8        crate_name: &str,
9        levels: usize,
10        verbose: bool,
11    ) -> Result<WorkspaceDependencyTree, WorkspaceError>;
12}
13
14#[async_trait]
15impl<P, H> BuildWorkspaceSubTree for Workspace<P, H>
16where
17    P: From<PathBuf> + AsRef<Path> + Clone + Send + Sync + 'static,
18    H: CrateHandleInterface<P> + Send + Sync + 'static,
19{
20    /// Builds a tree focusing on a single crate as the “root” (entrypoint),
21    /// recursing down its dependencies up to `levels`.
22    async fn build_workspace_subtree(
23        &self,
24        crate_name: &str,
25        levels: usize,
26        verbose: bool,
27    ) -> Result<WorkspaceDependencyTree, WorkspaceError> {
28        use petgraph::visit::EdgeRef;
29        use petgraph::{graph::Graph, Direction};
30        use std::collections::BTreeMap;
31        use tracing::{debug, error, info, trace};
32
33        trace!(
34            "Entering build_workspace_subtree(crate_name={:?}, levels={:?}, verbose={:?})",
35            crate_name,
36            levels,
37            verbose
38        );
39
40        // 1) Gather all crates in the workspace
41        let all_crates = self.crates();
42        debug!("Number of crates in workspace: {}", all_crates.len());
43
44        // Build a petgraph with (node=crate name), (edge=dependency).
45        let mut graph = Graph::<String, ()>::new();
46        let mut name_to_idx = BTreeMap::new();
47
48        // For storing info about each crate => version + path + internal deps
49        let mut crate_info: Vec<(String, Option<SemverVersion>, PathBuf, Vec<String>)> = Vec::new();
50
51        // Lock each crate handle, gather metadata
52        for arc_h in all_crates {
53            let guard = arc_h.lock().await;
54            let nm = guard.name().to_string();
55            trace!("Collecting metadata for crate: {}", nm);
56
57            let ver = match guard.version() {
58                Ok(v) => Some(v),
59                Err(e) => {
60                    error!("Error retrieving version for crate `{}`: {:?}", nm, e);
61                    None
62                }
63            };
64
65            let crate_path: PathBuf = guard.as_ref().to_path_buf();
66            let internal_deps = guard.internal_dependencies().await.unwrap_or_else(|e| {
67                error!(
68                    "Failed to retrieve internal_dependencies for crate `{}`: {:?}",
69                    nm, e
70                );
71                vec![]
72            });
73
74            let node_idx = graph.add_node(nm.clone());
75            name_to_idx.insert(nm.clone(), node_idx);
76
77            crate_info.push((nm, ver, crate_path, internal_deps));
78        }
79
80        // 2) Add edges for each “internal dependency”
81        for (src_name, _ver, _path, deps) in &crate_info {
82            let src_idx = name_to_idx[src_name];
83            for dep_name in deps {
84                if let Some(&dst_idx) = name_to_idx.get(dep_name) {
85                    graph.add_edge(src_idx, dst_idx, ());
86                } else {
87                    debug!("Dependency `{}` not found in name_to_idx, ignoring", dep_name);
88                }
89            }
90        }
91
92        // 3) Find the node index for `crate_name`
93        let root_idx = match name_to_idx.get(crate_name) {
94            Some(idx) => *idx,
95            None => {
96                error!(
97                    "Requested root crate `{}` not found in this workspace",
98                    crate_name
99                );
100                return Err(WorkspaceError::InvalidWorkspace {
101                    invalid_workspace_path: self.as_ref().to_path_buf(),
102                });
103            }
104        };
105
106        // 4) Build up a single root node for that crate
107        let (root_ver, root_path) = crate_info_for_name(&crate_info, crate_name);
108        let mut root_node = WorkspaceTreeNode::new(crate_name.to_string(), root_ver, root_path);
109
110        // 5) Recurse down its dependencies if levels > 0
111        if levels > 0 {
112            build_children_rec(
113                &graph,
114                &crate_info,
115                root_idx,
116                1,
117                levels,
118                &mut root_node,
119            );
120        }
121
122        // 6) Return a tree with just one root
123        let tree = WorkspaceDependencyTree::new(vec![root_node]);
124        info!(
125            "build_workspace_subtree => returning tree with 1 root: `{}`",
126            crate_name
127        );
128        Ok(tree)
129    }
130}
131
132#[cfg(test)]
133mod test_build_workspace_subtree {
134    use super::*;
135
136    /// Helper that creates a mock workspace directory with two crates:
137    /// crateA depends on crateB.
138    /// Then returns the path so we can build the workspace from it.
139    async fn create_mock_workspace_with_deps() -> PathBuf {
140        let tmp = tempdir().expect("Failed to create temp dir");
141        let root_path = tmp.path().to_path_buf();
142
143        // Write top-level Cargo.toml for the workspace
144        let cargo_toml_content = r#"
145            [workspace]
146            members = ["crateA","crateB"]
147        "#;
148        {
149            let mut f = std::fs::File::create(root_path.join("Cargo.toml"))
150                .expect("Failed to create workspace Cargo.toml");
151            f.write_all(cargo_toml_content.as_bytes())
152                .expect("Failed to write workspace Cargo.toml");
153        }
154
155        // 1) crateB => no internal dependencies
156        let crate_b = root_path.join("crateB");
157        std::fs::create_dir(&crate_b).expect("Failed to create crateB dir");
158        {
159            let mut f = std::fs::File::create(crate_b.join("Cargo.toml"))
160                .expect("Failed to create crateB Cargo.toml");
161            let contents = r#"
162                [package]
163                name = "crateB"
164                version = "0.1.0"
165
166                [dependencies]
167                # no internal dependencies
168            "#;
169            f.write_all(contents.as_bytes())
170                .expect("Failed to write crateB Cargo.toml");
171        }
172
173        // 2) crateA => depends on crateB (path-based)
174        let crate_a = root_path.join("crateA");
175        std::fs::create_dir(&crate_a).expect("Failed to create crateA dir");
176        {
177            let mut f = std::fs::File::create(crate_a.join("Cargo.toml"))
178                .expect("Failed to create crateA Cargo.toml");
179            let contents = r#"
180                [package]
181                name = "crateA"
182                version = "0.2.0"
183
184                [dependencies]
185                crateB = { path = "../crateB" }
186            "#;
187            f.write_all(contents.as_bytes())
188                .expect("Failed to write crateA Cargo.toml");
189        }
190
191        root_path
192    }
193
194    #[traced_test]
195    async fn test_build_workspace_subtree_crateA() {
196        let ws_path = create_mock_workspace_with_deps().await;
197        let workspace = Workspace::<PathBuf, CrateHandle>::new(&ws_path)
198            .await
199            .expect("Should build workspace from mock directory");
200
201        // crateA depends on crateB, so subtree for crateA should contain both A and B
202        let tree_a = workspace
203            .build_workspace_subtree("crateA", 5, false)
204            .await
205            .expect("Should build subtree for crateA");
206
207        let rendered_a = tree_a.render(true, false);
208        info!("Subtree for crateA:\n{}", rendered_a);
209        assert!(
210            rendered_a.contains("crateA"),
211            "Should contain crateA in subtree"
212        );
213        assert!(
214            rendered_a.contains("crateB"),
215            "Should contain crateB in crateA's subtree"
216        );
217
218        // crateB => no dependencies
219        let tree_b = workspace
220            .build_workspace_subtree("crateB", 5, false)
221            .await
222            .expect("Should build subtree for crateB");
223
224        let rendered_b = tree_b.render(true, false);
225        trace!("Subtree for crateB:\n{}", rendered_b);
226        assert!(
227            rendered_b.contains("crateB"),
228            "Should contain crateB in subtree"
229        );
230        assert!(
231            !rendered_b.contains("crateA"),
232            "crateB has no parent => should not contain crateA"
233        );
234    }
235}