changepacks_core/
proejct_finder.rs

1use std::path::Path;
2
3use crate::project::Project;
4use anyhow::Result;
5use async_trait::async_trait;
6
7#[async_trait]
8pub trait ProjectFinder: std::fmt::Debug + Send + Sync {
9    fn projects(&self) -> Vec<&Project>;
10    fn projects_mut(&mut self) -> Vec<&mut Project>;
11    fn project_files(&self) -> &[&str];
12    async fn visit(&mut self, path: &Path, relative_path: &Path) -> Result<()>;
13    fn check_changed(&mut self, path: &Path) -> Result<()> {
14        for project in self.projects_mut() {
15            project.check_changed(path)?;
16        }
17        Ok(())
18    }
19    async fn test(&self) -> Result<()> {
20        Ok(())
21    }
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27    use crate::{Language, Package, UpdateType, Workspace};
28    use async_trait::async_trait;
29    use std::collections::HashSet;
30    use std::path::PathBuf;
31
32    #[derive(Debug)]
33    struct MockPackage {
34        name: Option<String>,
35        path: PathBuf,
36        relative_path: PathBuf,
37        changed: bool,
38        dependencies: HashSet<String>,
39    }
40
41    impl MockPackage {
42        fn new(name: &str, path: &str) -> Self {
43            Self {
44                name: Some(name.to_string()),
45                path: PathBuf::from(path),
46                relative_path: PathBuf::from(path),
47                changed: false,
48                dependencies: HashSet::new(),
49            }
50        }
51    }
52
53    #[async_trait]
54    impl Package for MockPackage {
55        fn name(&self) -> Option<&str> {
56            self.name.as_deref()
57        }
58        fn version(&self) -> Option<&str> {
59            Some("1.0.0")
60        }
61        fn path(&self) -> &Path {
62            &self.path
63        }
64        fn relative_path(&self) -> &Path {
65            &self.relative_path
66        }
67        async fn update_version(&mut self, _update_type: UpdateType) -> Result<()> {
68            Ok(())
69        }
70        fn is_changed(&self) -> bool {
71            self.changed
72        }
73        fn language(&self) -> Language {
74            Language::Node
75        }
76        fn dependencies(&self) -> &HashSet<String> {
77            &self.dependencies
78        }
79        fn add_dependency(&mut self, dep: &str) {
80            self.dependencies.insert(dep.to_string());
81        }
82        fn set_changed(&mut self, changed: bool) {
83            self.changed = changed;
84        }
85        fn default_publish_command(&self) -> String {
86            "echo test".to_string()
87        }
88    }
89
90    #[derive(Debug)]
91    struct MockWorkspace {
92        name: Option<String>,
93        path: PathBuf,
94        relative_path: PathBuf,
95        changed: bool,
96        dependencies: HashSet<String>,
97    }
98
99    impl MockWorkspace {
100        fn new(name: &str, path: &str) -> Self {
101            Self {
102                name: Some(name.to_string()),
103                path: PathBuf::from(path),
104                relative_path: PathBuf::from(path),
105                changed: false,
106                dependencies: HashSet::new(),
107            }
108        }
109    }
110
111    #[async_trait]
112    impl Workspace for MockWorkspace {
113        fn name(&self) -> Option<&str> {
114            self.name.as_deref()
115        }
116        fn path(&self) -> &Path {
117            &self.path
118        }
119        fn relative_path(&self) -> &Path {
120            &self.relative_path
121        }
122        fn version(&self) -> Option<&str> {
123            Some("1.0.0")
124        }
125        async fn update_version(&mut self, _update_type: UpdateType) -> Result<()> {
126            Ok(())
127        }
128        fn language(&self) -> Language {
129            Language::Node
130        }
131        fn dependencies(&self) -> &HashSet<String> {
132            &self.dependencies
133        }
134        fn add_dependency(&mut self, dep: &str) {
135            self.dependencies.insert(dep.to_string());
136        }
137        fn is_changed(&self) -> bool {
138            self.changed
139        }
140        fn set_changed(&mut self, changed: bool) {
141            self.changed = changed;
142        }
143        fn default_publish_command(&self) -> String {
144            "echo test".to_string()
145        }
146    }
147
148    #[derive(Debug)]
149    struct MockProjectFinder {
150        projects: Vec<Project>,
151    }
152
153    impl MockProjectFinder {
154        fn new() -> Self {
155            Self { projects: vec![] }
156        }
157
158        fn with_package(mut self, package: MockPackage) -> Self {
159            self.projects.push(Project::Package(Box::new(package)));
160            self
161        }
162
163        fn with_workspace(mut self, workspace: MockWorkspace) -> Self {
164            self.projects.push(Project::Workspace(Box::new(workspace)));
165            self
166        }
167    }
168
169    #[async_trait]
170    impl ProjectFinder for MockProjectFinder {
171        fn projects(&self) -> Vec<&Project> {
172            self.projects.iter().collect()
173        }
174
175        fn projects_mut(&mut self) -> Vec<&mut Project> {
176            self.projects.iter_mut().collect()
177        }
178
179        fn project_files(&self) -> &[&str] {
180            &["package.json"]
181        }
182
183        async fn visit(&mut self, _path: &Path, _relative_path: &Path) -> Result<()> {
184            Ok(())
185        }
186    }
187
188    #[test]
189    fn test_project_finder_check_changed() {
190        let package = MockPackage::new("test", "/project/package.json");
191        let mut finder = MockProjectFinder::new().with_package(package);
192
193        // Check a file that's in the project directory
194        finder
195            .check_changed(Path::new("/project/src/index.js"))
196            .unwrap();
197
198        // The project should be marked as changed
199        assert!(finder.projects()[0].is_changed());
200    }
201
202    #[test]
203    fn test_project_finder_check_changed_multiple_projects() {
204        let package1 = MockPackage::new("pkg1", "/project1/package.json");
205        let package2 = MockPackage::new("pkg2", "/project2/package.json");
206        let mut finder = MockProjectFinder::new()
207            .with_package(package1)
208            .with_package(package2);
209
210        // Check a file in project1 only
211        finder
212            .check_changed(Path::new("/project1/src/index.js"))
213            .unwrap();
214
215        // Only project1 should be changed
216        assert!(finder.projects()[0].is_changed());
217        assert!(!finder.projects()[1].is_changed());
218    }
219
220    #[tokio::test]
221    async fn test_project_finder_test() {
222        let finder = MockProjectFinder::new();
223        let result = finder.test().await;
224        assert!(result.is_ok());
225    }
226
227    #[test]
228    fn test_project_finder_with_workspace() {
229        let workspace = MockWorkspace::new("root", "/project/package.json");
230        let mut finder = MockProjectFinder::new().with_workspace(workspace);
231
232        finder
233            .check_changed(Path::new("/project/src/index.js"))
234            .unwrap();
235
236        assert!(finder.projects()[0].is_changed());
237    }
238}