workspacer_workspace_mock/
mock.rs

1// ---------------- [ File: workspacer-workspace-mock/src/mock.rs ]
2crate::ix!();
3
4lazy_static! {
5    /// A global registry of *any* typed MockWorkspace instances keyed by `PathBuf`.
6    /// We store them as `Box<dyn Any + Send + Sync>` so they can be downcast to the appropriate
7    /// generic type at runtime. This allows us to store multiple `MockWorkspace<P,H>` types
8    /// while keeping a single global map.
9    static ref MOCK_WORKSPACE_REGISTRY: AsyncMutex<HashMap<PathBuf, Box<dyn std::any::Any + Send + Sync>>> 
10        = AsyncMutex::new(HashMap::new());
11}
12
13impl MockWorkspace<PathBuf, MockCrateHandle> {
14    /// Registers this `MockWorkspace<PathBuf, MockCrateHandle>` in the global registry so that
15    /// subsequent calls to `MockWorkspace::<PathBuf, MockCrateHandle>::new(&path)` will pick up
16    /// these exact simulation settings for the given path.
17    pub async fn register_in_global(&self) {
18        let path: PathBuf = self.path().to_path_buf();
19        trace!("Registering MockWorkspace<PathBuf,MockCrateHandle> in global map, path={:?}", path);
20        let mut lock = MOCK_WORKSPACE_REGISTRY.lock().await;
21        // We store it as a `Box<dyn Any + Send + Sync>` so we can downcast later:
22        lock.insert(path.clone(), Box::new(self.clone()));
23        info!("MockWorkspace<PathBuf,MockCrateHandle> with path={:?} has been registered", path);
24    }
25}
26
27#[derive(Builder, MutGetters, Getters, Debug, Clone)]
28#[builder(setter(into))]
29#[getset(get = "pub", get_mut = "pub")]
30pub struct MockWorkspace<P, H>
31where
32    H: Clone + CrateHandleInterface<P>,
33    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
34{
35    /// A mock notion of the workspace path.
36    /// Often used as a "root" path for the workspace in tests.
37    path: P,
38
39    /// A list of crates in this mock workspace.
40    /// Each crate is itself wrapped in an Arc<AsyncMutex<...>> so the calling code
41    /// can lock and operate on them.
42    #[builder(default = "Vec::new()")]
43    crates: Vec<Arc<AsyncMutex<H>>>,
44
45    /// If `simulate_missing_cargo_toml` is true, then we pretend that there's
46    /// no top-level Cargo.toml, causing `AsyncTryFrom::new(...)` to fail
47    /// with `WorkspaceError::InvalidWorkspace`.
48    #[builder(default = "false")]
49    simulate_missing_cargo_toml: bool,
50
51    /// If `simulate_not_a_workspace` is true, then we pretend that the top-level
52    /// Cargo.toml exists but does NOT contain a `[workspace]` table,
53    /// causing `AsyncTryFrom::new(...)` to fail
54    /// with `WorkspaceError::ActuallyInSingleCrate`.
55    #[builder(default = "false")]
56    simulate_not_a_workspace: bool,
57
58    /// If `simulate_failed_integrity` is true, calls to `validate_integrity()`
59    /// will fail directly, simulating an overall workspace integrity error.
60    #[builder(default = "false")]
61    simulate_failed_integrity: bool,
62
63    /// If `simulate_no_crates` is true, calls to `AsyncFindItems::find_items(..)`
64    /// return an empty Vec, simulating that no crates were discovered in the workspace.
65    #[builder(default = "false")]
66    simulate_no_crates: bool,
67}
68
69impl<P, H> MockWorkspace<P, H>
70where
71    H: Clone + CrateHandleInterface<P>,
72    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Clone + Send + Sync + 'async_trait,
73{
74    /// A convenience constructor returning a "fully valid" mock workspace:
75    /// - Has a top-level Cargo.toml (simulate_missing_cargo_toml = false)
76    /// - Declares itself to be a workspace (simulate_not_a_workspace = false)
77    /// - Integrity checks pass (simulate_failed_integrity = false)
78    /// - It has some crates in `crates` (you can decide how many).
79    /// - `simulate_no_crates = false` so that `AsyncFindItems::find_items(...)` will
80    ///   return those crates.
81    pub fn fully_valid_config() -> Self {
82        trace!("MockWorkspace::fully_valid_config constructor called");
83        // For demonstration, let's add two crates into the workspace.
84        // You might choose to store real `MockCrateHandle`s or
85        // any `H: CrateHandleInterface<P>` to represent them.
86        // We'll assume H == MockCrateHandle in typical usage,
87        // but here we won't force that type.
88        // We'll just "pretend" we have an empty list if we don't control H's constructor.
89        // Adjust as needed.
90
91        // If `H` has a convenience constructor, you might do:
92        // let crate_a = Arc::new(AsyncMutex::new(H::new_from_mock("crateA")?));
93        // let crate_b = Arc::new(AsyncMutex::new(H::new_from_mock("crateB")?));
94        // For simplicity, we'll keep it empty unless you have a known H constructor.
95        let crates_list = vec![];
96
97        // Build our mock workspace
98        MockWorkspaceBuilder::default()
99            .path(PathBuf::from("/fake/mock/workspace/path")) // or any arbitrary "root"
100            .crates(crates_list)
101            .simulate_missing_cargo_toml(false)
102            .simulate_not_a_workspace(false)
103            .simulate_failed_integrity(false)
104            .simulate_no_crates(false)
105            .build()
106            .unwrap()
107    }
108}
109
110#[async_trait]
111impl<P, H> AsyncTryFrom<P> for MockWorkspace<P, H>
112where
113    for<'async_trait> H: Clone + CrateHandleInterface<P> + Send + Sync + 'async_trait,
114    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
115{
116    type Error = WorkspaceError;
117
118    async fn new(path: &P) -> Result<Self, Self::Error> {
119        trace!("MockWorkspace::AsyncTryFrom::new called, path={:?}", path.as_ref());
120        let path_buf = path.as_ref().to_path_buf();
121
122        // 1) See if there's a pre-registered MockWorkspace in the global map.
123        //    If found, attempt a downcast to `MockWorkspace<P,H>`.
124        {
125            let lock = MOCK_WORKSPACE_REGISTRY.lock().await;
126            if let Some(boxed_any) = lock.get(&path_buf) {
127                debug!("Found a pre-registered Box<dyn Any> for path={:?}", path_buf);
128                if let Some(existing_ws) = boxed_any.downcast_ref::<MockWorkspace<P, H>>() {
129                    trace!("Successfully downcast to MockWorkspace<P,H>, applying simulation checks");
130
131                    if *existing_ws.simulate_missing_cargo_toml() {
132                        error!("simulate_missing_cargo_toml=true => returning InvalidWorkspace");
133                        return Err(WorkspaceError::InvalidWorkspace {
134                            invalid_workspace_path: path_buf,
135                        });
136                    }
137
138                    if *existing_ws.simulate_not_a_workspace() {
139                        error!("simulate_not_a_workspace=true => returning ActuallyInSingleCrate");
140                        return Err(WorkspaceError::ActuallyInSingleCrate {
141                            path: path_buf,
142                        });
143                    }
144
145                    info!("Returning a clone of the pre-registered MockWorkspace<P,H>");
146                    return Ok(existing_ws.clone());
147                } else {
148                    warn!("Failed downcasting pre-registered item to MockWorkspace<P,H>; ignoring it");
149                }
150            }
151        }
152
153        // 2) Otherwise, build a new default "fully_valid_config" instance and apply checks
154        let mut ws = Self::fully_valid_config();
155        ws.path = path.clone();
156
157        if *ws.simulate_missing_cargo_toml() {
158            error!("simulate_missing_cargo_toml=true => returning InvalidWorkspace");
159            return Err(WorkspaceError::InvalidWorkspace {
160                invalid_workspace_path: path_buf,
161            });
162        }
163
164        if *ws.simulate_not_a_workspace() {
165            error!("simulate_not_a_workspace=true => returning ActuallyInSingleCrate");
166            return Err(WorkspaceError::ActuallyInSingleCrate {
167                path: path_buf,
168            });
169        }
170
171        info!("Returning a new fully_valid_config-based MockWorkspace<P,H>");
172        Ok(ws)
173    }
174}
175
176impl<P, H> WorkspaceInterface<P, H> for MockWorkspace<P, H>
177where
178    for<'async_trait> H: Clone + CrateHandleInterface<P> + 'async_trait,
179    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
180{
181    // This is a marker trait aggregator. All sub-traits must be implemented.
182}
183
184impl<P, H> GetCrates<P, H> for MockWorkspace<P, H>
185where
186    H: Clone + CrateHandleInterface<P>,
187    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
188{
189    fn crates(&self) -> &[Arc<AsyncMutex<H>>] {
190        trace!("MockWorkspace::GetCrates::crates called");
191        self.crates()
192    }
193}
194
195impl<P, H> GetCratesMut<P, H> for MockWorkspace<P, H>
196where
197    H: Clone + CrateHandleInterface<P>,
198    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
199{
200    fn crates_mut(&mut self) -> &mut Vec<Arc<AsyncMutex<H>>> {
201        trace!("MockWorkspace::GetCratesMut::crates called");
202        self.crates_mut()
203    }
204}
205
206impl<P, H> NumCrates for MockWorkspace<P, H>
207where
208    H: Clone + CrateHandleInterface<P>,
209    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
210{
211    fn n_crates(&self) -> usize {
212        trace!("MockWorkspace::NumCrates::n_crates called");
213        self.crates().len()
214    }
215}
216
217#[async_trait]
218impl<P, H> ValidateIntegrity for MockWorkspace<P, H>
219where
220    H: Clone + CrateHandleInterface<P>,
221    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
222{
223    type Error = WorkspaceError;
224
225    async fn validate_integrity(&self) -> Result<(), Self::Error> {
226        trace!("MockWorkspace::validate_integrity called");
227
228        // If simulate_missing_cargo_toml is true, pretend there's no top-level Cargo.toml:
229        if *self.simulate_missing_cargo_toml() {
230            error!("simulate_missing_cargo_toml=true => returning InvalidWorkspace");
231            return Err(WorkspaceError::InvalidWorkspace {
232                invalid_workspace_path: self.path().as_ref().to_path_buf(),
233            });
234        }
235
236        // If simulate_not_a_workspace is true, pretend there's no [workspace] table:
237        if *self.simulate_not_a_workspace() {
238            error!("simulate_not_a_workspace=true => returning ActuallyInSingleCrate");
239            return Err(WorkspaceError::ActuallyInSingleCrate {
240                path: self.path().as_ref().to_path_buf(),
241            });
242        }
243
244        // If simulate_failed_integrity is set, we bail:
245        if *self.simulate_failed_integrity() {
246            error!("simulate_failed_integrity=true => returning InvalidWorkspace");
247            return Err(WorkspaceError::InvalidWorkspace {
248                invalid_workspace_path: self.path().as_ref().to_path_buf(),
249            });
250        }
251
252        // If simulate_no_crates => do nothing special here; the logic 
253        // for find_items might produce an empty list, etc.
254
255        // Otherwise, validate each crate:
256        for c in self.crates().iter() {
257            let guard = c.lock().await;
258            guard.validate_integrity().await?;
259        }
260
261        info!("MockWorkspace: integrity validation passed");
262        Ok(())
263    }
264}
265
266#[async_trait]
267impl<P, H> FindCrateByName<P, H> for MockWorkspace<P, H>
268where
269    H: Clone + CrateHandleInterface<P>,
270    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
271{
272    async fn find_crate_by_name(&self, name: &str) -> Option<Arc<AsyncMutex<H>>> {
273        trace!("MockWorkspace::FindCrateByName::find_crate_by_name called, name={}", name);
274        for crate_arc in self.crates().iter() {
275            let guard = crate_arc.lock().await;
276            if guard.name() == name {
277                debug!("MockWorkspace: found crate matching name='{}'", name);
278                return Some(Arc::clone(crate_arc));
279            }
280        }
281        info!("MockWorkspace: no crate matching name='{}'", name);
282        None
283    }
284}
285
286#[async_trait]
287impl<P, H> GetAllCrateNames for MockWorkspace<P, H>
288where
289    H: Clone + CrateHandleInterface<P>,
290    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
291{
292    async fn get_all_crate_names(&self) -> Vec<String> {
293        trace!("MockWorkspace::GetAllCrateNames::get_all_crate_names called");
294        let mut names = vec![];
295        for crate_arc in self.crates().iter() {
296            let guard = crate_arc.lock().await;
297            names.push(guard.name().to_string());
298        }
299        info!("MockWorkspace: returning crate names: {:?}", names);
300        names
301    }
302}
303
304impl<P, H> AsRef<Path> for MockWorkspace<P, H>
305where
306    H: Clone + CrateHandleInterface<P>,
307    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
308{
309    fn as_ref(&self) -> &Path {
310        trace!("MockWorkspace::as_ref called, returning path={:?}", self.path().as_ref());
311        self.path().as_ref()
312    }
313}
314
315#[async_trait]
316impl<P, H> AsyncFindItems for MockWorkspace<P, H>
317where
318    H: Clone + CrateHandleInterface<P> + Send + Sync,
319    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
320{
321    type Item = Arc<AsyncMutex<H>>;
322    type Error = WorkspaceError;
323
324    async fn find_items(_path: &Path) -> Result<Vec<Self::Item>, Self::Error> {
325        trace!("MockWorkspace::AsyncFindItems::find_items called (static), ignoring path={:?}", _path);
326        // Because this is a static method, we can't reference `self`.
327        // We'll just check if the mock is simulating "no crates" or not.
328        // For a real mock scenario, you might store the "simulate_xxx" state in a global
329        // or pass a static reference. To keep it simple, we'll always return an empty list
330        // or a single crate if you prefer. We'll do an empty list for demonstration.
331
332        info!("MockWorkspace: returning an empty crate list from find_items");
333        Ok(vec![])
334    }
335}
336
337#[async_trait]
338impl<P, H> AsyncPathValidator for MockWorkspace<P, H>
339where
340    H: Clone + CrateHandleInterface<P> + Send + Sync,
341    for<'async_trait> P: Clone + From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
342{
343    async fn is_valid(path: &Path) -> bool {
344        trace!("MockWorkspace::AsyncPathValidator::is_valid called for path={:?}", path);
345        // We'll just see if the path ends with "simulate_invalid" for demonstration.
346        // In real usage, you'd do something else. For now, let's pretend it's always valid:
347        debug!("MockWorkspace: returning true for is_valid(...) by default");
348        true
349    }
350}
351
352// ----------------------------------------------------------------------
353// Tests for MockWorkspace
354// ----------------------------------------------------------------------
355#[cfg(test)]
356mod test_mock_workspace {
357    use super::*;
358
359    // Because we can't combine #[traced_test] with #[tokio::test],
360    // we can manually create a tokio runtime if we want to run async code.
361
362    #[traced_test]
363    fn test_fully_valid_config_works() {
364        let mock_ws = MockWorkspace::<PathBuf, MockCrateHandle>::fully_valid_config();
365        assert!(!mock_ws.simulate_missing_cargo_toml(), "Should default to false");
366        assert!(!mock_ws.simulate_not_a_workspace(), "Should default to false");
367        assert!(!mock_ws.simulate_failed_integrity(), "Should default to false");
368        assert!(!mock_ws.simulate_no_crates(), "Should default to false");
369
370        // Check that n_crates() matches the crates we inserted
371        // (Here, we did an empty list in the code above, so it should be 0.)
372        assert_eq!(mock_ws.n_crates(), 0, "Currently we have an empty crates list");
373    }
374
375    #[traced_test]
376    async fn test_mock_workspace_new_missing_cargo_toml_fails() {
377        // The key fix is to use a UNIQUE path per test so we don't overwrite or conflict
378        // with another global registry entry in a different test that sets different flags.
379        let path = PathBuf::from("/fake/mock/workspace/path_for_missing_cargo_toml");
380
381        trace!("test_mock_workspace_new_missing_cargo_toml_fails starting");
382        // 1) Build a "fully valid" MockWorkspace
383        let mut failing_ws = MockWorkspace::<PathBuf, MockCrateHandle>::fully_valid_config();
384        // 2) Provide a custom path so we don't conflict with other tests
385        *failing_ws.path_mut() = path.clone();
386        // 3) Toggle the simulation flag to simulate missing Cargo.toml
387        *failing_ws.simulate_missing_cargo_toml_mut() = true;
388        // 4) Register in the global map, making sure to `await`
389        failing_ws.register_in_global().await;
390
391        // 5) Now call `MockWorkspace::new(...)`, which should see that
392        //    `simulate_missing_cargo_toml = true` for this path
393        let result = MockWorkspace::<PathBuf, MockCrateHandle>::new(&path).await;
394
395        // 6) Check that we do, in fact, get `WorkspaceError::InvalidWorkspace`
396        assert!(result.is_err(), "Should fail with simulate_missing_cargo_toml=true");
397        match result.err().unwrap() {
398            WorkspaceError::InvalidWorkspace { .. } => {
399                info!("Got expected WorkspaceError::InvalidWorkspace");
400            }
401            other => {
402                panic!("Expected InvalidWorkspace error, got: {:?}", other);
403            }
404        }
405    }
406
407    #[traced_test]
408    async fn test_mock_workspace_new_not_a_workspace_fails() {
409        // Again, use a UNIQUE path for not-a-workspace scenario
410        let path = PathBuf::from("/fake/mock/workspace/path_for_not_a_workspace");
411
412        trace!("test_mock_workspace_new_not_a_workspace_fails starting");
413        // 1) Build a "fully valid" MockWorkspace
414        let mut failing_ws = MockWorkspace::<PathBuf, MockCrateHandle>::fully_valid_config();
415        // 2) Provide a custom path so we don't conflict with other tests
416        *failing_ws.path_mut() = path.clone();
417        // 3) Toggle the simulation flag
418        *failing_ws.simulate_not_a_workspace_mut() = true;
419        // 4) Register in the global map
420        failing_ws.register_in_global().await;
421
422        // 5) This should find the pre-registered configuration and produce
423        //    `WorkspaceError::ActuallyInSingleCrate`
424        let result = MockWorkspace::<PathBuf, MockCrateHandle>::new(&path).await;
425        assert!(result.is_err(), "Should fail with simulate_not_a_workspace=true");
426
427        match result.err().unwrap() {
428            WorkspaceError::ActuallyInSingleCrate { .. } => {
429                info!("Got expected WorkspaceError::ActuallyInSingleCrate");
430            }
431            other => {
432                panic!("Expected ActuallyInSingleCrate error, got: {:?}", other);
433            }
434        }
435    }
436
437    #[traced_test]
438    async fn test_mock_workspace_validate_integrity_fails_when_simulated() {
439        let mut ws = MockWorkspace::<PathBuf, MockCrateHandle>::fully_valid_config();
440        *ws.simulate_failed_integrity_mut() = true;
441        let result = ws.validate_integrity().await;
442        assert!(result.is_err(), "Should fail if simulate_failed_integrity=true");
443    }
444
445    #[traced_test]
446    async fn test_mock_workspace_find_crate_by_name() {
447        // We'll put some crates in the list and see if we can find them by name
448        // For demonstration, we'll create 2 MockCrateHandles with names "crateA" and "crateB"
449        let crate_a = Arc::new(AsyncMutex::new(
450            MockCrateHandle::fully_valid_config()
451                .to_builder()
452                .crate_name("crateA")
453                .build()
454                .unwrap()
455        ));
456        let crate_b = Arc::new(AsyncMutex::new(
457            MockCrateHandle::fully_valid_config()
458                .to_builder()
459                .crate_name("crateB")
460                .build()
461                .unwrap()
462        ));
463
464        let ws = MockWorkspaceBuilder::<PathBuf, MockCrateHandle>::default()
465            .path(PathBuf::from("/fake/mock/workspace/path"))
466            .crates(vec![crate_a.clone(), crate_b.clone()])
467            .build()
468            .unwrap();
469
470        assert_eq!(ws.n_crates(), 2, "We have 2 crates total");
471        let found = ws.find_crate_by_name("crateB").await;
472        assert!(found.is_some(), "Should find crateB by name");
473        let found = found.unwrap();
474        assert_eq!(found.lock().await.name(), "crateB");
475    }
476
477    #[traced_test]
478    async fn test_mock_workspace_get_all_crate_names() {
479        // Similar to above
480        let crate_1 = Arc::new(AsyncMutex::new(
481            MockCrateHandle::fully_valid_config()
482                .to_builder()
483                .crate_name("crateAlpha")
484                .build()
485                .unwrap()
486        ));
487        let crate_2 = Arc::new(AsyncMutex::new(
488            MockCrateHandle::fully_valid_config()
489                .to_builder()
490                .crate_name("crateBeta")
491                .build()
492                .unwrap()
493        ));
494
495        let ws = MockWorkspaceBuilder::<PathBuf, MockCrateHandle>::default()
496            .path(PathBuf::from("/fake/mock/workspace/path"))
497            .crates(vec![crate_1.clone(), crate_2.clone()])
498            .build()
499            .unwrap();
500
501        let names = ws.get_all_crate_names().await;
502        assert_eq!(names.len(), 2, "Should have 2 names total");
503        assert!(names.contains(&"crateAlpha".to_string()));
504        assert!(names.contains(&"crateBeta".to_string()));
505    }
506}