@@ -209,3 +209,80 @@ async fn get_crate_source_paths_no_cargo_project_returns_empty() {
209209 let paths = crawler. get_crate_source_paths ( & options_at ( tmp. path ( ) ) ) . await . unwrap ( ) ;
210210 assert ! ( paths. is_empty( ) , "non-Cargo dir must return empty paths" ) ;
211211}
212+
213+ // ── parse_dir_name_version fallback (via crawl_all) ────────────
214+
215+ /// Crate directory whose Cargo.toml has `version.workspace = true`
216+ /// (no concrete `version =` field) — the crawler must fall back to
217+ /// parsing `<name>-<version>` from the directory name. Exercises
218+ /// `parse_dir_name_version` (cargo_crawler.rs:357-372).
219+ #[ tokio:: test]
220+ async fn crawl_all_falls_back_to_dir_name_when_workspace_version ( ) {
221+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
222+ // <name>-<version> directory; Cargo.toml has workspace version.
223+ let pkg_dir = tmp. path ( ) . join ( "serde_json-1.0.120" ) ;
224+ tokio:: fs:: create_dir ( & pkg_dir) . await . unwrap ( ) ;
225+ tokio:: fs:: write (
226+ pkg_dir. join ( "Cargo.toml" ) ,
227+ "[package]\n name = \" serde_json\" \n version.workspace = true\n edition = \" 2021\" \n " ,
228+ )
229+ . await
230+ . unwrap ( ) ;
231+
232+ let crawler = CargoCrawler ;
233+ let opts = CrawlerOptions {
234+ cwd : tmp. path ( ) . to_path_buf ( ) ,
235+ global : true ,
236+ global_prefix : Some ( tmp. path ( ) . to_path_buf ( ) ) ,
237+ batch_size : 100 ,
238+ } ;
239+ let result = crawler. crawl_all ( & opts) . await ;
240+ assert_eq ! ( result. len( ) , 1 ) ;
241+ assert_eq ! ( result[ 0 ] . name, "serde_json" ) ;
242+ assert_eq ! ( result[ 0 ] . version, "1.0.120" ) ;
243+ }
244+
245+ #[ tokio:: test]
246+ async fn crawl_all_skips_dir_without_cargo_toml ( ) {
247+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
248+ // Directory shaped like a crate but no Cargo.toml — must be skipped.
249+ let pkg_dir = tmp. path ( ) . join ( "not_a_crate-1.0.0" ) ;
250+ tokio:: fs:: create_dir ( & pkg_dir) . await . unwrap ( ) ;
251+
252+ let crawler = CargoCrawler ;
253+ let opts = CrawlerOptions {
254+ cwd : tmp. path ( ) . to_path_buf ( ) ,
255+ global : true ,
256+ global_prefix : Some ( tmp. path ( ) . to_path_buf ( ) ) ,
257+ batch_size : 100 ,
258+ } ;
259+ let result = crawler. crawl_all ( & opts) . await ;
260+ assert ! ( result. is_empty( ) , "dir without Cargo.toml must be skipped" ) ;
261+ }
262+
263+ /// `verify_crate_at_path`'s fallback path: Cargo.toml has workspace
264+ /// version, find_by_purls compares dir name. Exercises the
265+ /// fallback arm in `verify_crate_at_path` (L335-L348).
266+ #[ tokio:: test]
267+ async fn find_by_purls_verify_fallback_via_dir_name ( ) {
268+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
269+ let pkg = tmp. path ( ) . join ( "workspace-crate-0.1.0" ) ;
270+ tokio:: fs:: create_dir ( & pkg) . await . unwrap ( ) ;
271+ // Cargo.toml has workspace version → triggers fallback.
272+ tokio:: fs:: write (
273+ pkg. join ( "Cargo.toml" ) ,
274+ "[package]\n name = \" workspace-crate\" \n version.workspace = true\n " ,
275+ )
276+ . await
277+ . unwrap ( ) ;
278+
279+ let crawler = CargoCrawler ;
280+ let result = crawler
281+ . find_by_purls (
282+ tmp. path ( ) ,
283+ & [ "pkg:cargo/workspace-crate@0.1.0" . to_string ( ) ] ,
284+ )
285+ . await
286+ . unwrap ( ) ;
287+ assert_eq ! ( result. len( ) , 1 , "verify must fall back to dir name" ) ;
288+ }
0 commit comments