workspacer_crate/
crate_handle.rs

1// ---------------- [ File: workspacer-crate/src/crate_handle.rs ]
2crate::ix!();
3
4#[derive(Builder,Getters,Debug,Clone)]
5#[getset(get="pub")]
6#[builder(setter(into))]
7pub struct CrateHandle {
8    crate_path:        PathBuf,
9    cargo_toml_handle: Arc<AsyncMutex<CargoToml>>,
10}
11
12impl Named for CrateHandle {
13    fn name(&self) -> std::borrow::Cow<'_, str> {
14        use tracing::{trace, debug, info};
15
16        trace!("Entering CrateHandle::name");
17
18        // Clone so the async closure can be 'static without borrowing &self.
19        let cargo_toml_handle = self.cargo_toml_handle.clone();
20
21        let package_name = sync_run_async(async move {
22            trace!("Locking cargo_toml_handle in async block");
23            let guard = cargo_toml_handle.lock().await;
24            let name = guard
25                .package_name()
26                .expect("Expected a valid package name");
27            debug!("Retrieved package name from guard: {:?}", name);
28            name
29        });
30
31        info!("Synchronous retrieval of package name succeeded: {}", package_name);
32
33        let trimmed = package_name.trim_matches('"').to_string();
34        trace!("Trimmed package name: {}", trimmed);
35
36        std::borrow::Cow::Owned(trimmed)
37    }
38}
39
40impl Versioned for CrateHandle {
41    type Error = CrateError;
42
43    fn version(&self) -> Result<semver::Version, Self::Error> {
44        trace!("CrateHandle::version called");
45
46        // We'll do an immediate read from the embedded CargoToml
47        let cargo_toml_arc = self.cargo_toml();
48
49        // Because `version()` is a sync method, we do a best-effort lock attempt:
50        // (If your code is guaranteed single-thread, a direct lock() is fine.)
51        let guard = match cargo_toml_arc.try_lock() {
52            Ok(g) => g,
53            Err(_) => {
54                error!("CrateHandle::version: cannot lock cargo_toml right now => returning a general error");
55                return Err(CrateError::CargoTomlIsLocked);
56            }
57        };
58
59        // Now parse the version from cargo_toml, mapping each error:
60        match guard.version() {
61            Ok(ver) => {
62                info!("CrateHandle: returning semver={}", ver);
63                Ok(ver)
64            }
65            Err(e) => {
66                error!("CrateHandle: cargo_toml.version() => encountered error: {:?}", e);
67                // Convert cargo_toml errors to crate errors
68                match e {
69                    CargoTomlError::FileNotFound { missing_file } => {
70                        // For example, unify that into IoError
71                        Err(CrateError::IoError {
72                            io_error: Arc::new(std::io::Error::new(
73                                std::io::ErrorKind::NotFound,
74                                format!("Cargo.toml missing at {}", missing_file.display()),
75                            )),
76                            context: format!("CrateHandle::version => no Cargo.toml at {:?}", missing_file),
77                        })
78                    }
79                    other => {
80                        // For invalid semver or anything else, we pass as CargoTomlError
81                        Err(CrateError::CargoTomlError(other))
82                    }
83                }
84            }
85        }
86    }
87}
88
89impl<P> CrateHandleInterface<P> for CrateHandle 
90where 
91    for<'async_trait> 
92    P
93    : HasCargoTomlPathBuf 
94    + HasCargoTomlPathBufSync
95    + AsRef<Path> 
96    + Send 
97    + Sync
98    + 'async_trait,
99
100    CrateError
101    : From<<P as HasCargoTomlPathBuf>::Error> 
102    + From<<P as HasCargoTomlPathBufSync>::Error>,
103{}
104
105#[async_trait]
106impl<P> AsyncTryFrom<P> for CrateHandle 
107where 
108    for<'async_trait> 
109    P
110    : HasCargoTomlPathBuf 
111    + HasCargoTomlPathBufSync 
112    + AsRef<Path> 
113    + Send 
114    + Sync
115    + 'async_trait,
116
117    CrateError
118    : From<<P as HasCargoTomlPathBuf>::Error> 
119    + From<<P as HasCargoTomlPathBufSync>::Error>,
120{
121    type Error = CrateError;
122
123    /// Initializes a crate handle from a given crate_path
124    async fn new(crate_path: &P) -> Result<Self,Self::Error> {
125
126        let cargo_toml_path = crate_path.cargo_toml_path_buf().await?;
127
128        let cargo_toml_handle = Arc::new(AsyncMutex::new(CargoToml::new(cargo_toml_path).await?));
129
130        Ok(Self {
131            cargo_toml_handle,
132            crate_path: crate_path.as_ref().to_path_buf(),
133        })
134    }
135}
136
137impl CrateHandle 
138{
139    /// Initializes a crate handle from a given crate_path
140    pub fn new_sync<P>(crate_path: &P) -> Result<Self,CrateError> 
141    where 
142        for<'async_trait> 
143            P
144                : HasCargoTomlPathBuf 
145                + HasCargoTomlPathBufSync 
146                + AsRef<Path> 
147                + Send 
148                + Sync
149                + 'async_trait,
150
151        CrateError
152            : From<<P as HasCargoTomlPathBuf>::Error> 
153            + From<<P as HasCargoTomlPathBufSync>::Error>
154    {
155
156        let cargo_toml_path = crate_path.cargo_toml_path_buf_sync()?;
157
158        let cargo_toml_handle = Arc::new(AsyncMutex::new(CargoToml::new_sync(cargo_toml_path)?));
159
160        Ok(Self {
161            cargo_toml_handle,
162            crate_path: crate_path.as_ref().to_path_buf(),
163        })
164    }
165}
166
167#[async_trait]
168impl ValidateIntegrity for CrateHandle {
169    type Error = CrateError;
170
171    async fn validate_integrity(&self) -> Result<(), Self::Error> {
172        trace!("CrateHandle::validate_integrity() - forcing a re-parse from disk to catch sabotage or missing fields.");
173
174        // 1) Ensure Cargo.toml is readable & has a valid version (no `.expect(...)`!)
175        let cargo_toml_arc = self.cargo_toml();
176        let ct_guard = cargo_toml_arc.lock().await;
177        match ct_guard.version() {
178            Err(e) => {
179                error!("CrateHandle: cargo_toml.version() => encountered error: {:?}", e);
180                // Convert cargo-toml errors to crate errors (FileNotFound => IoError, InvalidVersionFormat => CargoTomlError, etc)
181                match e {
182                    CargoTomlError::FileNotFound { missing_file } => {
183                        return Err(CrateError::IoError {
184                            io_error: Arc::new(std::io::Error::new(
185                                std::io::ErrorKind::NotFound,
186                                format!("Cargo.toml missing at {}", missing_file.display()),
187                            )),
188                            context: format!("validate_integrity: no Cargo.toml at {:?}", missing_file),
189                        });
190                    }
191                    _ => {
192                        return Err(CrateError::CargoTomlError(e));
193                    }
194                }
195            }
196            Ok(ver) => {
197                trace!("CrateHandle::validate_integrity => version is valid ({})", ver);
198            }
199        }
200
201        // 2) Additional checks like "has src/main.rs" or "has lib.rs"
202        self.check_src_directory_contains_valid_files()?;
203
204        // 3) Check README
205        self.check_readme_exists()?;
206
207        info!("CrateHandle::validate_integrity passed successfully");
208        Ok(())
209    }
210}
211
212impl CheckIfSrcDirectoryContainsValidFiles for CrateHandle {
213
214    /// Checks if the `src/` directory contains a `lib.rs` or `main.rs`
215    fn check_src_directory_contains_valid_files(&self) -> Result<(), CrateError> {
216        let src_dir = self.as_ref().join("src");
217        let main_rs = src_dir.join("main.rs");
218        let lib_rs = src_dir.join("lib.rs");
219
220        if !main_rs.exists() && !lib_rs.exists() {
221            return Err(CrateError::FileNotFound {
222                missing_file: src_dir.join("main.rs or lib.rs"),
223            });
224        }
225
226        // It's okay if both exist
227        Ok(())
228    }
229}
230
231impl CheckIfReadmeExists for CrateHandle {
232
233    /// Checks if `README.md` exists
234    fn check_readme_exists(&self) -> Result<(), CrateError> {
235        let readme_path = self.as_ref().join("README.md");
236        if !readme_path.exists() {
237            return Err(CrateError::FileNotFound {
238                missing_file: readme_path,
239            });
240        }
241        Ok(())
242    }
243}
244
245#[async_trait]
246impl GetReadmePath for CrateHandle {
247
248    /// Asynchronously returns the path to the `README.md` if it exists
249    async fn readme_path(&self) -> Result<Option<PathBuf>, CrateError> {
250        let readme_path = self.crate_path.join("README.md");
251        if fs::metadata(&readme_path).await.is_ok() {
252            Ok(Some(readme_path))
253        } else {
254            Ok(None)
255        }
256    }
257}
258
259#[async_trait]
260impl GetSourceFilesWithExclusions for CrateHandle {
261
262    /// Asynchronously returns a list of source files (`.rs`) in the `src/` directory, excluding specified files
263    async fn source_files_excluding(&self, exclude_files: &[&str]) -> Result<Vec<PathBuf>, CrateError> {
264        self.get_files_in_dir_with_exclusions("src", "rs", exclude_files).await
265    }
266}
267
268#[async_trait]
269impl GetTestFiles for CrateHandle {
270
271    /// Asynchronously returns a list of test files (`.rs`) in the `tests/` directory
272    async fn test_files(&self) -> Result<Vec<PathBuf>, CrateError> {
273        self.get_files_in_dir("tests", "rs").await
274    }
275}
276
277impl HasTestsDirectory for CrateHandle {
278
279    fn has_tests_directory(&self) -> bool {
280        self.crate_path.join("tests").exists()
281    }
282}
283
284#[async_trait]
285impl GetFilesInDirectory for CrateHandle {
286
287    /// Asynchronously returns a list of files with the given extension in the specified directory
288    async fn get_files_in_dir(&self, dir_name: &str, extension: &str) -> Result<Vec<PathBuf>, CrateError> {
289        self.get_files_in_dir_with_exclusions(dir_name, extension, &[]).await
290    }
291}
292
293#[async_trait]
294impl GetFilesInDirectoryWithExclusions for CrateHandle {
295
296    /// Asynchronously returns a list of files with the given extension in the specified directory,
297    /// excluding specified file names.
298    async fn get_files_in_dir_with_exclusions(
299        &self,
300        dir_name: &str,
301        extension: &str,
302        exclude_files: &[&str]
303    ) -> Result<Vec<PathBuf>, CrateError> {
304        let dir_path = self.crate_path.join(dir_name);
305
306        if !fs::metadata(&dir_path).await.is_ok() {
307            return Err(CrateError::DirectoryNotFound {
308                missing_directory: dir_path,
309            });
310        }
311
312        let mut files = vec![];
313
314        let mut entries = fs::read_dir(dir_path)
315            .await
316            .map_err(|e| DirectoryError::ReadDirError {io: e.into() })?;
317
318        while let Some(entry) 
319            = entries.next_entry()
320            .await
321            .map_err(|e| DirectoryError::GetNextEntryError {io: e.into() })? 
322        {
323            let path = entry.path();
324            let file_name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| {
325                CrateError::FailedToGetFileNameForPath {
326                    path: path.to_path_buf()
327                }
328            })?;
329
330            if path.extension().and_then(|e| e.to_str()) == Some(extension) && !exclude_files.contains(&file_name) {
331                files.push(path);
332            }
333        }
334
335        Ok(files)
336    }
337}
338
339impl HasCargoToml for CrateHandle {
340
341    fn cargo_toml(&self) -> Arc<AsyncMutex<dyn CargoTomlInterface>> {
342        self.cargo_toml_handle.clone()
343    }
344}
345
346impl CrateHandle {
347
348    /// sometimes we need to do this, but do try not to
349    pub fn cargo_toml_direct(&self) -> Arc<AsyncMutex<CargoToml>> {
350        self.cargo_toml_handle.clone()
351    }
352}
353
354
355impl AsRef<Path> for CrateHandle {
356    /// Allows CrateHandle to be used as a path by referencing crate_path
357    fn as_ref(&self) -> &Path {
358        &self.crate_path
359    }
360}
361
362// Implementation for `CrateHandle`. This is private, not re-exported.
363#[async_trait]
364impl GetInternalDependencies for CrateHandle {
365    async fn internal_dependencies(&self) -> Result<Vec<String>, CrateError> {
366        // 1) Lock the underlying CargoToml 
367        let cargo_arc = self.cargo_toml();
368        let cargo_guard = cargo_arc.lock().await;
369
370        // 2) Extract local path-based dependencies from the CargoToml
371        //    For example: 
372        //    [dependencies]
373        //    foo = { path = "../foo" }
374        // We take "foo" as the dependency name.
375        let mut results = Vec::new();
376
377        let empty = toml::value::Table::new();
378
379        let root_table = cargo_guard.get_content()
380            .as_table()
381            .unwrap_or_else(|| {
382                // If top-level is not a table => we skip or return an error
383                // For demonstration we do an empty table
384                // Or possibly: return Err(CrateError::...);
385                &empty
386            });
387
388        // We look up `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`
389        for deps_key in &["dependencies", "dev-dependencies", "build-dependencies"] {
390            if let Some(deps_val) = root_table.get(*deps_key).and_then(|v| v.as_table()) {
391                // Now iterate each key => sub-table or inline table
392                for (dep_name, dep_item) in deps_val.iter() {
393                    // If it is a table or inline table with "path" = "..." => that’s local
394                    if let Some(dep_tbl) = dep_item.as_table() {
395                        if dep_tbl.get("path").is_some() {
396                            // We consider this an internal dependency
397                            results.push(dep_name.to_string());
398                        }
399                    } 
400                    else if dep_item.is_str() {
401                        // If the user wrote `foo = "1.2.3"` => no path => not internal
402                    }
403                    // else: Possibly do more checks if you want
404                }
405            }
406        }
407
408        Ok(results)
409    }
410}
411
412#[cfg(test)]
413mod test_crate_handle {
414    use super::*;
415    use std::path::{Path, PathBuf};
416    use tempfile::{tempdir, TempDir};
417    use tokio::fs::{File, create_dir_all};
418    use tokio::io::AsyncWriteExt;
419
420    // A small helper that creates and writes arbitrary text to a file.
421    async fn write_file(file_path: &Path, content: &str) {
422        if let Some(parent_dir) = file_path.parent() {
423            create_dir_all(parent_dir)
424                .await
425                .expect("Failed to create parent directories");
426        }
427        let mut f = File::create(file_path)
428            .await
429            .unwrap_or_else(|e| panic!("Could not create file {}: {e}", file_path.display()));
430        f.write_all(content.as_bytes())
431            .await
432            .unwrap_or_else(|e| panic!("Failed to write to file {}: {e}", file_path.display()));
433    }
434
435    // Creates a basic "Cargo.toml" content.  
436    // By default, includes `[package] name, version, authors, license`.
437    fn minimal_cargo_toml(name: &str, version: &str) -> String {
438        format!(
439            r#"[package]
440name = "{name}"
441version = "{version}"
442authors = ["Some Body"]
443license = "MIT"
444"#,
445        )
446    }
447
448    /// Helper to build a `CrateHandle` by placing a Cargo.toml file (and optional other files)
449    /// in a temporary directory, then calling `CrateHandle::new(...)`.
450    /// We return the TempDir too, so it stays alive while tests run.
451    async fn create_crate_handle_in_temp(
452        crate_name: &str,
453        crate_version: &str,
454        create_src_dir: bool,
455        create_tests_dir: bool,
456        create_readme: bool,
457        main_or_lib: Option<&str>, // "main" or "lib" or None
458    ) -> (TempDir, CrateHandle) {
459        let tmp_dir = tempdir().expect("Failed to create temp dir");
460        let root_path = tmp_dir.path().to_path_buf();
461
462        // Write Cargo.toml
463        let cargo_toml_content = minimal_cargo_toml(crate_name, crate_version);
464        let cargo_toml_path = root_path.join("Cargo.toml");
465        write_file(&cargo_toml_path, &cargo_toml_content).await;
466
467        // Optionally create src and main.rs or lib.rs
468        if create_src_dir {
469            if let Some(which) = main_or_lib {
470                let file_name = format!("{which}.rs");
471                let file_path = root_path.join("src").join(file_name);
472                write_file(&file_path, "// sample content").await;
473            }
474        }
475
476        // Optionally create tests directory
477        if create_tests_dir {
478            let test_file_path = root_path.join("tests").join("test_basic.rs");
479            write_file(&test_file_path, "// test file content").await;
480        }
481
482        // Optionally create README.md
483        if create_readme {
484            let readme_path = root_path.join("README.md");
485            write_file(&readme_path, "# My Crate\nSome description.").await;
486        }
487
488        // Minimal struct to implement `HasCargoTomlPathBuf`
489        #[derive(Clone)]
490        struct TempCratePath(PathBuf);
491
492        impl AsRef<Path> for TempCratePath {
493            fn as_ref(&self) -> &Path {
494                self.0.as_ref()
495            }
496        }
497
498        // Create the input object
499        let temp_crate_path = TempCratePath(root_path.clone());
500
501        // Finally call CrateHandle::new
502        let handle = CrateHandle::new(&temp_crate_path)
503            .await
504            .expect("Failed to create CrateHandle from temp directory");
505
506        (tmp_dir, handle)
507    }
508
509    // ------------------------------------------------------------------------
510    // Actual tests
511    // ------------------------------------------------------------------------
512
513    /// 1) Test that name() and version() work for a minimal crate.
514    #[tokio::test]
515    async fn test_name_and_version() {
516        let (_tmp_dir, handle) =
517            create_crate_handle_in_temp("test_crate", "0.1.0", false, false, false, None).await;
518        eprintln!("handle: {:#?}", handle);
519        assert_eq!(handle.name(), "test_crate");
520        eprintln!("handle.name(): {:#?}", handle.name());
521        let ver = handle.version().expect("Expected valid version");
522        eprintln!("handle.version(): {:#?}", handle.version());
523        assert_eq!(ver.to_string(), "0.1.0");
524    }
525
526    /// 2) Test check_src_directory_contains_valid_files when we have src/main.rs
527    #[tokio::test]
528    async fn test_check_src_directory_contains_valid_files_main_rs() {
529        let (_tmp_dir, handle) = create_crate_handle_in_temp(
530            "mycrate",
531            "0.1.0",
532            true,  // create src
533            false, // no tests
534            false, // no readme
535            Some("main"),
536        )
537        .await;
538
539        // Should not error
540        handle.check_src_directory_contains_valid_files().expect("Should find main.rs");
541    }
542
543    /// 3) Test check_src_directory_contains_valid_files when we have neither main.rs nor lib.rs => error
544    #[tokio::test]
545    async fn test_check_src_directory_contains_valid_files_missing_main_and_lib() {
546        let (_tmp_dir, handle) = create_crate_handle_in_temp(
547            "mycrate",
548            "0.1.0",
549            true,  // create src dir
550            false, // no tests
551            false, // no readme
552            None,  // no main or lib
553        )
554        .await;
555
556        let result = handle.check_src_directory_contains_valid_files();
557        assert!(
558            result.is_err(),
559            "Expected an error because neither main.rs nor lib.rs is present"
560        );
561        match result {
562            Err(CrateError::FileNotFound { missing_file }) => {
563                let missing = missing_file.to_string_lossy();
564                assert!(
565                    missing.contains("main.rs or lib.rs"),
566                    "Error message should mention main.rs or lib.rs"
567                );
568            }
569            _ => panic!("Expected CrateError::FileNotFound with mention of main.rs or lib.rs"),
570        }
571    }
572
573    /// 4) Test check_readme_exists => success when README.md is present
574    #[tokio::test]
575    async fn test_check_readme_exists_ok() {
576        let (_tmp_dir, handle) = create_crate_handle_in_temp(
577            "mycrate",
578            "0.1.0",
579            true,   // src
580            false,  // tests
581            true,   // readme
582            Some("lib"),
583        )
584        .await;
585
586        // Should not error
587        handle.check_readme_exists().expect("README.md should exist");
588    }
589
590    /// 5) Test check_readme_exists => error when no README.md
591    #[tokio::test]
592    async fn test_check_readme_exists_missing() {
593        let (_tmp_dir, handle) = create_crate_handle_in_temp(
594            "mycrate",
595            "0.1.0",
596            true,  // src
597            false, // tests
598            false, // readme missing
599            Some("lib"),
600        )
601        .await;
602
603        let result = handle.check_readme_exists();
604        assert!(result.is_err());
605        match result {
606            Err(CrateError::FileNotFound { missing_file }) => {
607                let missing = missing_file.to_string_lossy();
608                assert!(
609                    missing.contains("README.md"),
610                    "Expected error referencing README.md"
611                );
612            }
613            _ => panic!("Expected CrateError::FileNotFound for missing README.md"),
614        }
615    }
616
617    /// 6) Test has_tests_directory => false if we never created it, true if we did.
618    #[tokio::test]
619    async fn test_has_tests_directory() {
620        let (_tmp_dir, handle_no_tests) = create_crate_handle_in_temp(
621            "mycrate",
622            "0.1.0",
623            true,
624            false, // no tests
625            false,
626            Some("lib"),
627        )
628        .await;
629        assert!(
630            !handle_no_tests.has_tests_directory(),
631            "Expected false, no tests/ folder"
632        );
633
634        let (_tmp_dir, handle_with_tests) = create_crate_handle_in_temp(
635            "mycrate",
636            "0.1.0",
637            true,
638            true, // yes tests
639            false,
640            Some("lib"),
641        )
642        .await;
643        assert!(
644            handle_with_tests.has_tests_directory(),
645            "Expected true, tests/ folder created"
646        );
647    }
648
649    /// 7) Test get_source_files_excluding and get_test_files
650    #[tokio::test]
651    async fn test_file_enumeration_in_source_and_tests() {
652        let (_tmp_dir, handle) = create_crate_handle_in_temp(
653            "mycrate",
654            "0.1.0",
655            true,  // create src
656            true,  // create tests
657            true,  // readme
658            Some("lib"),
659        )
660        .await;
661
662        // Add one more file in src
663        let extra_src = handle.as_ref().join("src").join("extra.rs");
664        write_file(&extra_src, "// extra file").await;
665
666        // Add one more file in tests
667        let extra_test = handle.as_ref().join("tests").join("extra_test.rs");
668        write_file(&extra_test, "// extra test file").await;
669
670        // Now ask for the source files
671        let src_files = handle
672            .source_files_excluding(&[])
673            .await
674            .expect("Should list src files");
675        // We expect 2: lib.rs + extra.rs
676        assert_eq!(src_files.len(), 2, "Should find 2 .rs files in src");
677
678        // Now check test files
679        let test_files = handle.test_files().await.expect("Should list test files");
680        // We expect 2: test_basic.rs + extra_test.rs
681        assert_eq!(test_files.len(), 2, "Should find 2 .rs files in tests");
682    }
683
684    /// 8) Test source_files_excluding to ensure we skip any excluded file(s).
685    #[tokio::test]
686    async fn test_source_files_excluding() {
687        let (_tmp_dir, handle) = create_crate_handle_in_temp(
688            "excluded_crate",
689            "0.1.0",
690            true,
691            false,
692            true,
693            Some("lib"),
694        )
695        .await;
696
697        // Add one more file in src
698        let extra_src = handle.as_ref().join("src").join("exclude_me.rs");
699        write_file(&extra_src, "// exclude me").await;
700
701        // If we exclude "exclude_me.rs", we should only see "lib.rs"
702        let src_files = handle
703            .source_files_excluding(&["exclude_me.rs"])
704            .await
705            .unwrap();
706        assert_eq!(src_files.len(), 1, "Expected to exclude exclude_me.rs");
707        let only_file_name = src_files[0]
708            .file_name()
709            .unwrap()
710            .to_string_lossy()
711            .into_owned();
712        assert_eq!(only_file_name, "lib.rs");
713    }
714
715    /// Test validate_integrity => ensures the crate has Cargo.toml, a valid src file, and readme, etc.
716    /// (check_src_directory_contains_valid_files + check_readme_exists).
717    #[traced_test]
718    async fn test_validate_integrity() {
719        // a) valid scenario
720        let (_tmp_dir, handle_ok) = create_crate_handle_in_temp(
721            "integrity_crate",
722            "0.1.1",
723            true,
724            false,
725            true,
726            Some("lib"),
727        )
728        .await;
729        let res_ok = handle_ok.validate_integrity().await;
730        assert!(res_ok.is_ok(), "Expected valid integrity with a src file and README");
731
732        // b) missing main.rs/lib.rs => should fail
733        let (_tmp_dir, handle_bad_src) = create_crate_handle_in_temp(
734            "bad_src_crate",
735            "0.1.0",
736            true,
737            false,
738            true,
739            None, // no main/lib
740        )
741        .await;
742        let res_bad_src = handle_bad_src.validate_integrity().await;
743        assert!(
744            res_bad_src.is_err(),
745            "Expected integrity check to fail with missing main.rs/lib.rs"
746        );
747
748        // c) missing README => fail
749        let (_tmp_dir, handle_no_readme) = create_crate_handle_in_temp(
750            "no_readme_crate",
751            "0.1.0",
752            true,
753            false,
754            false,
755            Some("main"),
756        )
757        .await;
758        let res_no_readme = handle_no_readme.validate_integrity().await;
759        assert!(res_no_readme.is_err(), "Expected missing README.md error");
760        match res_no_readme {
761            Err(CrateError::FileNotFound { missing_file }) => {
762                assert!(
763                    missing_file.ends_with("README.md"),
764                    "Expected error referencing missing README.md"
765                );
766            }
767            _ => panic!("Expected FileNotFound referencing README.md"),
768        }
769    }
770}