workspacer_crate_mock/
mock.rs

1// ---------------- [ File: workspacer-crate-mock/src/mock.rs ]
2crate::ix!();
3
4/// A fully functional mock implementing the same `CrateHandleInterface<P>`
5/// as the real CrateHandle, but with configurable behaviors.
6/// 
7/// The goal is to simulate a crate's contents (like `src/main.rs`, `README.md`, etc.)
8/// as well as the embedded `CargoTomlInterface` without touching the real filesystem.
9#[derive(Builder, MutGetters, Getters, Debug, Clone)]
10#[builder(setter(into))]
11#[getset(get = "pub", get_mut = "pub")]
12pub struct MockCrateHandle {
13    /// A mock notion of where this crate is located. Often used as a "root" path.
14    crate_path: PathBuf,
15
16    /// The "name" returned by `Named::name()`.
17    crate_name: String,
18
19    /// The "version" returned by `Versioned::version()`.
20    crate_version: String,
21
22    /// If `is_private` is `true`, `is_private()` returns `Ok(true)`. Otherwise, `Ok(false)`.
23    is_private_crate: bool,
24
25    /// If `simulate_invalid_version` is `true`, then calls to `version()` will fail
26    /// with a `CrateError::InvalidVersionFormat` or similar.
27    simulate_invalid_version: bool,
28
29    /// If `simulate_missing_main_or_lib` is `true`, calls to
30    /// `check_src_directory_contains_valid_files()` will fail,
31    /// simulating that there's no main.rs nor lib.rs.
32    simulate_missing_main_or_lib: bool,
33
34    /// If `simulate_missing_readme` is `true`, calls to `check_readme_exists()` will fail,
35    /// simulating that there's no README.md.
36    simulate_missing_readme: bool,
37
38    /// If `simulate_no_tests_directory` is `true`, `has_tests_directory()` returns false,
39    /// and any attempts to list test files may return an empty list or error.
40    simulate_no_tests_directory: bool,
41
42    /// If `simulate_failed_integrity` is `true`, calls to `validate_integrity()` fail directly,
43    /// simulating a failing integrity check for any reason you choose.
44    simulate_failed_integrity: bool,
45
46    /// A mock table of "file path -> file contents" for `read_file_string`.
47    /// If a path is not found in this map, we return an error in `read_file_string`.
48    #[builder(default = "HashMap::new()")]
49    file_contents: HashMap<PathBuf, String>,
50
51    /// A mock list of source files for `source_files_excluding` usage.
52    /// We do a simple path-based or filename-based approach.
53    /// (In a real scenario, you might store them in `file_contents` as well.)
54    #[builder(default = "Vec::new()")]
55    source_files: Vec<PathBuf>,
56
57    /// A mock list of test files for `test_files`.
58    #[builder(default = "Vec::new()")]
59    test_files: Vec<PathBuf>,
60
61    /// An embedded mock of `CargoTomlInterface`. This can be a `MockCargoToml`
62    /// or something that *itself* is fully configurable.
63    /// We store it in an `Arc<Mutex<...>>` so we can hand it out in `HasCargoToml`.
64    #[builder(default = "Arc::new(AsyncMutex::new(MockCargoToml::fully_valid_config()))")]
65    mock_cargo_toml: Arc<AsyncMutex<MockCargoToml>>,
66}
67
68#[async_trait]
69impl GetInternalDependencies for MockCrateHandle {
70    async fn internal_dependencies(&self) -> Result<Vec<String>, CrateError> {
71        todo!();
72    }
73}
74
75impl MockCrateHandle {
76    /// A convenience constructor returning a "fully valid" mock crate:
77    /// - Has a name "mock_crate"
78    /// - Has version "1.2.3"
79    /// - Not private
80    /// - Integrity checks pass
81    /// - Contains main.rs or lib.rs
82    /// - Has a README
83    /// - Has a tests directory
84    /// - File reading will succeed for any path in `file_contents`
85    /// - Has a fully valid `MockCargoToml` inside
86    pub fn fully_valid_config() -> Self {
87        trace!("MockCrateHandle::fully_valid_config constructor called");
88        let mut file_map = HashMap::new();
89        file_map.insert(PathBuf::from("README.md"), "# Mock Crate\n".into());
90        file_map.insert(PathBuf::from("src/main.rs"), "// mock main".into());
91        file_map.insert(PathBuf::from("tests/test_basic.rs"), "// mock test".into());
92
93        MockCrateHandleBuilder::default()
94            .crate_path("fake/mock/crate/path")
95            .crate_name("mock_crate")
96            .crate_version("1.2.3")
97            .is_private_crate(false)
98            .simulate_invalid_version(false)
99            .simulate_missing_main_or_lib(false)
100            .simulate_missing_readme(false)
101            .simulate_no_tests_directory(false)
102            .simulate_failed_integrity(false)
103            .file_contents(file_map)
104            .source_files(vec![PathBuf::from("src/main.rs")])
105            .test_files(vec![PathBuf::from("tests/test_basic.rs")])
106            // By default, embed a fully-valid MockCargoToml
107            .mock_cargo_toml(Arc::new(AsyncMutex::new(MockCargoToml::fully_valid_config())))
108            .build()
109            .unwrap()
110    }
111
112    /// A constructor that simulates an invalid version scenario (Versioned trait fails).
113    pub fn invalid_version_config() -> Self {
114        trace!("MockCrateHandle::invalid_version_config constructor called");
115        Self::fully_valid_config()
116            .to_builder()
117            .simulate_invalid_version(true)
118            .build()
119            .unwrap()
120    }
121
122    /// A constructor that simulates a crate missing main.rs/lib.rs.
123    pub fn missing_main_or_lib_config() -> Self {
124        trace!("MockCrateHandle::missing_main_or_lib_config constructor called");
125        Self::fully_valid_config()
126            .to_builder()
127            .simulate_missing_main_or_lib(true)
128            .build()
129            .unwrap()
130    }
131
132    /// A constructor that simulates a crate missing its README.md.
133    pub fn missing_readme_config() -> Self {
134        trace!("MockCrateHandle::missing_readme_config constructor called");
135        // We'll remove "README.md" from file_contents too
136        let mut mc = Self::fully_valid_config();
137        mc.file_contents_mut().remove(&PathBuf::from("README.md"));
138        mc = mc
139            .to_builder()
140            .simulate_missing_readme(true)
141            .build()
142            .unwrap();
143        mc
144    }
145
146    /// A constructor that simulates no tests directory.
147    pub fn no_tests_directory_config() -> Self {
148        trace!("MockCrateHandle::no_tests_directory_config constructor called");
149        // We'll remove any test files
150        let mut mc = Self::fully_valid_config();
151        mc.test_files_mut().clear();
152        mc.file_contents_mut().remove(&PathBuf::from("tests/test_basic.rs"));
153        mc = mc
154            .to_builder()
155            .simulate_no_tests_directory(true)
156            .build()
157            .unwrap();
158        mc
159    }
160
161    /// A constructor that simulates the crate being private.
162    pub fn private_crate_config() -> Self {
163        trace!("MockCrateHandle::private_crate_config constructor called");
164        Self::fully_valid_config()
165            .to_builder()
166            .is_private_crate(true)
167            .build()
168            .unwrap()
169    }
170
171    /// A constructor that simulates an overall integrity failure (for any reason).
172    pub fn failed_integrity_config() -> Self {
173        trace!("MockCrateHandle::failed_integrity_config constructor called");
174        Self::fully_valid_config()
175            .to_builder()
176            .simulate_failed_integrity(true)
177            .build()
178            .unwrap()
179    }
180
181    /// Helper for using the builder pattern on an existing instance (so we can mutate some fields).
182    pub fn to_builder(&self) -> MockCrateHandleBuilder {
183        let mut builder = MockCrateHandleBuilder::default();
184
185        // Copy all fields from self into the builder
186        builder
187            .crate_path(self.crate_path().clone())
188            .crate_name(self.crate_name().clone())
189            .crate_version(self.crate_version().clone())
190            .is_private_crate(*self.is_private_crate())
191            .simulate_invalid_version(*self.simulate_invalid_version())
192            .simulate_missing_main_or_lib(*self.simulate_missing_main_or_lib())
193            .simulate_missing_readme(*self.simulate_missing_readme())
194            .simulate_no_tests_directory(*self.simulate_no_tests_directory())
195            .simulate_failed_integrity(*self.simulate_failed_integrity())
196            .file_contents(self.file_contents().clone())
197            .source_files(self.source_files().clone())
198            .test_files(self.test_files().clone())
199            .mock_cargo_toml(self.mock_cargo_toml().clone());
200
201        builder
202    }
203}
204
205// ----------------------------------------------------------------------
206// Implement all the traits for MockCrateHandle
207// ----------------------------------------------------------------------
208
209impl Named for MockCrateHandle {
210    fn name(&self) -> Cow<'_, str> {
211        Cow::Owned(self.crate_name().clone())
212    }
213}
214
215impl Versioned for MockCrateHandle {
216    type Error = CrateError;
217
218    fn version(&self) -> Result<semver::Version, Self::Error> {
219        trace!("MockCrateHandle::version called");
220        if *self.simulate_invalid_version() {
221            error!("MockCrateHandle: simulating invalid version parse error");
222            return Err(CrateError::SimulatedInvalidVersionFormat);
223        }
224
225        // Forward to the embedded MockCargoToml, so we see any updated [package].version
226        let cargo_toml_arc = self.mock_cargo_toml();
227        // Because this is a sync method, we'll do a best-effort try_lock:
228        let guard = match cargo_toml_arc.try_lock() {
229            Ok(g) => g,
230            Err(_) => {
231                error!("MockCrateHandle: unable to lock mock_cargo_toml => returning error");
232                return Err(CrateError::CouldNotLockMockCargoTomlInVersion);
233            }
234        };
235        let parsed = guard.version().map_err(|e| CrateError::CargoTomlError(e))?;
236        info!("MockCrateHandle: returning semver={}", parsed);
237        Ok(parsed)
238    }
239}
240
241#[async_trait]
242impl IsPrivate for MockCrateHandle {
243    type Error = CrateError;
244
245    async fn is_private(&self) -> Result<bool, Self::Error> {
246        trace!("MockCrateHandle::is_private called");
247        Ok(*self.is_private_crate())
248    }
249}
250
251#[async_trait]
252impl ReadFileString for MockCrateHandle {
253    async fn read_file_string(&self, path: &Path) -> Result<String, CrateError> {
254        trace!("MockCrateHandle::read_file_string called with path={:?}", path);
255
256        // Convert to an owned PathBuf to do lookups
257        // If it's absolute or something, we still just do a map lookup.
258        let path_buf = path.to_path_buf();
259
260        // If the exact key is found, great. Otherwise, let's see if the path is relative to crate_path.
261        if let Some(contents) = self.file_contents().get(&path_buf) {
262            debug!("MockCrateHandle: found file contents by exact match in file_contents map");
263            return Ok(contents.clone());
264        }
265
266        // Otherwise, try joining with crate_path if it's not absolute
267        if !path_buf.is_absolute() {
268            let joined = self.crate_path().join(&path_buf);
269            if let Some(contents) = self.file_contents().get(&joined) {
270                debug!("MockCrateHandle: found file contents by joined path");
271                return Ok(contents.clone());
272            }
273        }
274
275        // If not found in the map, simulate "file not found" or a read error
276        error!(
277            "MockCrateHandle: no entry in file_contents for path={:?}, read failed",
278            path_buf
279        );
280        Err(CrateError::IoError {
281            io_error: Arc::new(std::io::Error::new(
282                std::io::ErrorKind::NotFound,
283                "File not found in MockCrateHandle",
284            )),
285            context: format!("Cannot read file at {:?}", path_buf),
286        })
287    }
288}
289
290impl CheckIfSrcDirectoryContainsValidFiles for MockCrateHandle {
291    fn check_src_directory_contains_valid_files(&self) -> Result<(), CrateError> {
292        trace!("MockCrateHandle::check_src_directory_contains_valid_files called");
293        if *self.simulate_missing_main_or_lib() {
294            error!("MockCrateHandle: simulating missing main.rs/lib.rs scenario");
295            return Err(CrateError::FileNotFound {
296                missing_file: self.crate_path().join("src").join("main.rs or lib.rs"),
297            });
298        }
299        info!("MockCrateHandle: main.rs or lib.rs is considered present");
300        Ok(())
301    }
302}
303
304impl CheckIfReadmeExists for MockCrateHandle {
305    fn check_readme_exists(&self) -> Result<(), CrateError> {
306        trace!("MockCrateHandle::check_readme_exists called");
307        if *self.simulate_missing_readme() {
308            error!("MockCrateHandle: simulating missing README.md");
309            return Err(CrateError::FileNotFound {
310                missing_file: self.crate_path().join("README.md"),
311            });
312        }
313        info!("MockCrateHandle: README.md is considered present");
314        Ok(())
315    }
316}
317
318#[async_trait]
319impl GetReadmePath for MockCrateHandle {
320    async fn readme_path(&self) -> Result<Option<PathBuf>, CrateError> {
321        trace!("MockCrateHandle::readme_path called");
322        if *self.simulate_missing_readme() {
323            warn!("MockCrateHandle: README not present");
324            Ok(None)
325        } else {
326            let path = self.crate_path().join("README.md");
327            info!("MockCrateHandle: returning Some({:?})", path);
328            Ok(Some(path))
329        }
330    }
331}
332
333#[async_trait]
334impl GetSourceFilesWithExclusions for MockCrateHandle {
335    async fn source_files_excluding(&self, exclude_files: &[&str]) -> Result<Vec<PathBuf>, CrateError> {
336        trace!(
337            "MockCrateHandle::source_files_excluding called, exclude_files={:?}",
338            exclude_files
339        );
340        let mut results = vec![];
341        for f in self.source_files().iter() {
342            let file_name = f.file_name().and_then(|ff| ff.to_str()).unwrap_or("");
343            if !exclude_files.contains(&file_name) {
344                results.push(f.clone());
345            }
346        }
347        Ok(results)
348    }
349}
350
351#[async_trait]
352impl GetTestFiles for MockCrateHandle {
353    async fn test_files(&self) -> Result<Vec<PathBuf>, CrateError> {
354        trace!("MockCrateHandle::test_files called");
355        if *self.simulate_no_tests_directory() {
356            // We could either return an empty list or an error. For now, let's just return empty.
357            info!("MockCrateHandle: simulating no tests directory => returning empty");
358            Ok(vec![])
359        } else {
360            Ok(self.test_files().clone())
361        }
362    }
363}
364
365impl HasTestsDirectory for MockCrateHandle {
366    fn has_tests_directory(&self) -> bool {
367        trace!("MockCrateHandle::has_tests_directory called");
368        !self.simulate_no_tests_directory()
369    }
370}
371
372#[async_trait]
373impl GetFilesInDirectory for MockCrateHandle {
374    async fn get_files_in_dir(
375        &self,
376        dir_name: &str,
377        _extension: &str,
378    ) -> Result<Vec<PathBuf>, CrateError> {
379        trace!("MockCrateHandle::get_files_in_dir called, dir_name={}", dir_name);
380        // We'll just unify with get_files_in_dir_with_exclusions and pass an empty exclude list.
381        let results = self
382            .get_files_in_dir_with_exclusions(dir_name, _extension, &[])
383            .await?;
384        Ok(results)
385    }
386}
387
388#[async_trait]
389impl GetFilesInDirectoryWithExclusions for MockCrateHandle {
390    async fn get_files_in_dir_with_exclusions(
391        &self,
392        dir_name: &str,
393        extension: &str,
394        exclude_files: &[&str],
395    ) -> Result<Vec<PathBuf>, CrateError> {
396        trace!(
397            "MockCrateHandle::get_files_in_dir_with_exclusions called, dir_name={}, extension={}, exclude_files={:?}",
398            dir_name,
399            extension,
400            exclude_files
401        );
402
403        // We'll gather from either source_files or test_files if they start with that dir
404        // or we can do a naive approach if you want. We'll do a simple approach here:
405        let mut results = vec![];
406        for f in self.source_files().iter().chain(self.test_files().iter()) {
407            let rel_str = f.to_string_lossy();
408            if rel_str.contains(dir_name) {
409                // Check extension
410                if f.extension().and_then(|ex| ex.to_str()) == Some(extension) {
411                    let file_name = f.file_name().and_then(|ff| ff.to_str()).unwrap_or("");
412                    if !exclude_files.contains(&file_name) {
413                        results.push(f.clone());
414                    }
415                }
416            }
417        }
418        info!(
419            "MockCrateHandle: returning {} file(s) for dir_name={}",
420            results.len(),
421            dir_name
422        );
423        Ok(results)
424    }
425}
426
427impl HasCargoToml for MockCrateHandle {
428    fn cargo_toml(&self) -> Arc<AsyncMutex<dyn CargoTomlInterface>> {
429        trace!("MockCrateHandle::cargo_toml called");
430        self.mock_cargo_toml().clone()
431    }
432}
433
434impl AsRef<Path> for MockCrateHandle {
435    fn as_ref(&self) -> &Path {
436        trace!("MockCrateHandle::as_ref called, returning crate_path={:?}", self.crate_path());
437        self.crate_path()
438    }
439}
440
441#[async_trait]
442impl GatherBinTargetNames for MockCrateHandle {
443    type Error = CrateError;
444
445    async fn gather_bin_target_names(&self) -> Result<Vec<String>, Self::Error> {
446        trace!("MockCrateHandle::gather_bin_target_names called");
447        // For the mock, let's just delegate to the embedded MockCargoToml's gather_bin_target_names
448        let bin_list = self.mock_cargo_toml()
449            .lock()
450            .await
451            .gather_bin_target_names()
452            .await?;
453
454        Ok(bin_list)
455    }
456}
457
458#[async_trait]
459impl ValidateIntegrity for MockCrateHandle {
460    type Error = CrateError;
461
462    async fn validate_integrity(&self) -> Result<(), Self::Error> {
463        trace!("MockCrateHandle::validate_integrity called");
464        if *self.simulate_failed_integrity() {
465            error!("MockCrateHandle: simulating overall integrity failure");
466            return Err(CrateError::SimulatedIntegrityFailureInMockCrate);
467        }
468        // Otherwise, do some checks
469        self.check_src_directory_contains_valid_files()?;
470        self.check_readme_exists()?;
471        // We might also check the embedded cargo toml's `validate_integrity` if we want
472        // For now, let's skip or do it:
473        self.mock_cargo_toml()
474            .lock()
475            .await
476            .validate_integrity()
477            .await
478            .map_err(|e| CrateError::CargoTomlError(e))?;
479
480        info!("MockCrateHandle: integrity validation passed");
481        Ok(())
482    }
483}
484
485// ----------------------------------------------------------------------
486// Implement `AsyncTryFrom<P>` for MockCrateHandle
487// This is needed to satisfy `CrateHandleInterface<P>`.
488// In real usage, you might load from some config, but here we simulate all.
489//
490// We'll just *ignore* the input parameter and return `Ok(Self::fully_valid_config())`
491// or we can do something else. We'll do a simple approach for demonstration:
492// ----------------------------------------------------------------------
493#[async_trait]
494impl<P> AsyncTryFrom<P> for MockCrateHandle
495where
496    for<'async_trait> P: HasCargoTomlPathBuf + HasCargoTomlPathBufSync + AsRef<Path> + Send + Sync + 'async_trait,
497    CrateError: From<<P as HasCargoTomlPathBuf>::Error>
498        + From<<P as HasCargoTomlPathBufSync>::Error>,
499{
500    type Error = CrateError;
501
502    async fn new(_crate_path: &P) -> Result<Self, Self::Error> {
503        trace!("MockCrateHandle::AsyncTryFrom::new called for a mock handle");
504        // For demonstration, we simply return a fully valid config.
505        // If you want to adjust based on `_crate_path`, you can do so.
506        Ok(MockCrateHandle::fully_valid_config())
507    }
508}
509
510// ----------------------------------------------------------------------
511// Implement the aggregator trait `CrateHandleInterface<P>`
512// ----------------------------------------------------------------------
513impl<P> CrateHandleInterface<P> for MockCrateHandle
514where
515    for<'async_trait> P: HasCargoTomlPathBuf + HasCargoTomlPathBufSync + AsRef<Path> + Send + Sync + 'async_trait,
516    CrateError: From<<P as HasCargoTomlPathBuf>::Error>
517        + From<<P as HasCargoTomlPathBufSync>::Error>,
518{}
519
520// ----------------------------------------------------------------------
521// TESTS
522// ----------------------------------------------------------------------
523#[cfg(test)]
524mod tests_mock_crate_handle {
525    use super::*;
526
527    // Basic test demonstrating usage of the default "fully_valid_config"
528    // and verifying that the mock calls work as expected.
529    #[traced_test]
530    async fn test_fully_valid_config_behaves_correctly() {
531        let mock = MockCrateHandle::fully_valid_config();
532
533        // 1) name() and version()
534        assert_eq!(mock.name(), "mock_crate");
535        let ver = mock.version().expect("Should parse version 1.2.3");
536        assert_eq!(ver.to_string(), "1.2.3");
537
538        // 2) is_private => false
539        let priv_check = mock.is_private().await.unwrap();
540        assert!(!priv_check, "Expected is_private_crate = false");
541
542        // 3) read_file_string => can we read "README.md" from the map?
543        let readme_contents = mock.read_file_string(Path::new("README.md")).await
544            .expect("Should find README.md in file_contents");
545        assert!(readme_contents.contains("# Mock Crate"));
546
547        // 4) check_src_directory_contains_valid_files => no error
548        mock.check_src_directory_contains_valid_files().expect("Should pass, as we have main.rs or lib.rs");
549
550        // 5) check_readme_exists => no error
551        mock.check_readme_exists().expect("Should pass, as we have README.md");
552
553        // 6) gather_bin_target_names => delegates to embedded MockCargoToml
554        // By default, the embedded MockCargoToml is "fully_valid_config" which might have no bin targets,
555        // so let's see if it's empty:
556        let bin_targets = mock.gather_bin_target_names().await.unwrap();
557        assert!(bin_targets.len() == 1, "Default fully_valid_config from MockCargoToml has a single bin target");
558    }
559
560    #[traced_test]
561    fn test_invalid_version_config_fails_versioned_trait() {
562        let mock = MockCrateHandle::invalid_version_config();
563        let ver_res = mock.version();
564        assert!(ver_res.is_err(), "Should fail version parse");
565    }
566
567    #[traced_test]
568    fn test_missing_main_or_lib_config_fails_src_check() {
569        let mock = MockCrateHandle::missing_main_or_lib_config();
570        let src_check = mock.check_src_directory_contains_valid_files();
571        assert!(src_check.is_err(), "Should fail because we simulate missing main.rs/lib.rs");
572    }
573
574    #[traced_test]
575    fn test_missing_readme_config_fails_readme_check() {
576        let mock = MockCrateHandle::missing_readme_config();
577        let readme_check = mock.check_readme_exists();
578        assert!(readme_check.is_err(), "Should fail because we simulate missing README.md");
579    }
580
581    #[traced_test]
582    fn test_no_tests_directory_config() {
583        let mock = MockCrateHandle::no_tests_directory_config();
584        assert!(!mock.has_tests_directory(), "Should simulate no tests directory");
585        let test_files = mock.test_files();
586        assert!(test_files.is_empty(), "No test files in this config");
587    }
588
589    #[traced_test]
590    async fn test_private_crate_config_returns_true_for_is_private() {
591        let mock = MockCrateHandle::private_crate_config();
592        let priv_check = mock.is_private().await.unwrap();
593        assert!(priv_check, "Should be private");
594    }
595
596    #[traced_test]
597    async fn test_failed_integrity_config() {
598        let mock = MockCrateHandle::failed_integrity_config();
599        let integrity_res = mock.validate_integrity().await;
600        assert!(integrity_res.is_err(), "Simulated integrity failure");
601    }
602
603    #[traced_test]
604    async fn test_read_file_string_map_lookup() {
605        let mut file_map = HashMap::new();
606        file_map.insert(PathBuf::from("src/main.rs"), "fn main() {}".into());
607        let mock = MockCrateHandle::fully_valid_config()
608            .to_builder()
609            .file_contents(file_map)
610            .build()
611            .unwrap();
612
613        let contents = mock.read_file_string(Path::new("src/main.rs")).await
614            .expect("Should find main.rs in the file map");
615        assert_eq!(contents, "fn main() {}");
616
617        // If we try to read a file not in the map, we get an error
618        let missing_res = mock.read_file_string(Path::new("non_existent_file.txt")).await;
619        assert!(missing_res.is_err(), "Expect an IoError for missing file path in the map");
620    }
621}