1crate::ix!();
3
4#[derive(Builder, MutGetters, Getters, Debug, Clone)]
10#[builder(setter(into))]
11#[getset(get = "pub", get_mut = "pub")]
12pub struct MockCrateHandle {
13 crate_path: PathBuf,
15
16 crate_name: String,
18
19 crate_version: String,
21
22 is_private_crate: bool,
24
25 simulate_invalid_version: bool,
28
29 simulate_missing_main_or_lib: bool,
33
34 simulate_missing_readme: bool,
37
38 simulate_no_tests_directory: bool,
41
42 simulate_failed_integrity: bool,
45
46 #[builder(default = "HashMap::new()")]
49 file_contents: HashMap<PathBuf, String>,
50
51 #[builder(default = "Vec::new()")]
55 source_files: Vec<PathBuf>,
56
57 #[builder(default = "Vec::new()")]
59 test_files: Vec<PathBuf>,
60
61 #[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 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 .mock_cargo_toml(Arc::new(AsyncMutex::new(MockCargoToml::fully_valid_config())))
108 .build()
109 .unwrap()
110 }
111
112 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 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 pub fn missing_readme_config() -> Self {
134 trace!("MockCrateHandle::missing_readme_config constructor called");
135 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 pub fn no_tests_directory_config() -> Self {
148 trace!("MockCrateHandle::no_tests_directory_config constructor called");
149 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 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 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 pub fn to_builder(&self) -> MockCrateHandleBuilder {
183 let mut builder = MockCrateHandleBuilder::default();
184
185 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
205impl 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 let cargo_toml_arc = self.mock_cargo_toml();
227 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 let path_buf = path.to_path_buf();
259
260 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 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 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 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 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 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 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 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 self.check_src_directory_contains_valid_files()?;
470 self.check_readme_exists()?;
471 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#[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 Ok(MockCrateHandle::fully_valid_config())
507 }
508}
509
510impl<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#[cfg(test)]
524mod tests_mock_crate_handle {
525 use super::*;
526
527 #[traced_test]
530 async fn test_fully_valid_config_behaves_correctly() {
531 let mock = MockCrateHandle::fully_valid_config();
532
533 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 let priv_check = mock.is_private().await.unwrap();
540 assert!(!priv_check, "Expected is_private_crate = false");
541
542 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 mock.check_src_directory_contains_valid_files().expect("Should pass, as we have main.rs or lib.rs");
549
550 mock.check_readme_exists().expect("Should pass, as we have README.md");
552
553 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 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}