1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use changepacks_core::{Package, Project, ProjectFinder};
4use std::{
5 collections::HashMap,
6 path::{Path, PathBuf},
7};
8use tokio::fs::read_to_string;
9
10use crate::{package::RustPackage, workspace::RustWorkspace};
11
12#[derive(Debug)]
14struct PendingWorkspacePackage {
15 name: Option<String>,
16 abs_path: PathBuf,
17 relative_path: PathBuf,
18 dependencies: Vec<String>,
19}
20
21#[derive(Debug)]
22pub struct RustProjectFinder {
23 projects: HashMap<PathBuf, Project>,
24 project_files: Vec<&'static str>,
25 workspace_package_version: Option<String>,
26 workspace_root_path: Option<PathBuf>,
27 pending_workspace_packages: Vec<PendingWorkspacePackage>,
28}
29
30impl Default for RustProjectFinder {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl RustProjectFinder {
37 #[must_use]
38 pub fn new() -> Self {
39 Self {
40 projects: HashMap::new(),
41 project_files: vec!["Cargo.toml"],
42 workspace_package_version: None,
43 workspace_root_path: None,
44 pending_workspace_packages: Vec::new(),
45 }
46 }
47}
48
49#[async_trait]
50impl ProjectFinder for RustProjectFinder {
51 fn projects(&self) -> Vec<&Project> {
52 self.projects.values().collect::<Vec<_>>()
53 }
54 fn projects_mut(&mut self) -> Vec<&mut Project> {
55 self.projects.values_mut().collect::<Vec<_>>()
56 }
57
58 fn project_files(&self) -> &[&str] {
59 &self.project_files
60 }
61
62 async fn visit(&mut self, path: &Path, relative_path: &Path) -> Result<()> {
63 if path.is_file()
64 && self.project_files().contains(
65 &path
66 .file_name()
67 .context(format!("File name not found - {}", path.display()))?
68 .to_str()
69 .context(format!("File name not found - {}", path.display()))?,
70 )
71 {
72 if self.projects.contains_key(path) {
73 return Ok(());
74 }
75 let cargo_toml = read_to_string(path).await?;
77 let cargo_toml: toml::Value = toml::from_str(&cargo_toml)?;
78
79 let mut dep_names = Vec::new();
81 if let Some(deps) = cargo_toml.get("dependencies").and_then(|d| d.as_table()) {
82 for (dep_name, value) in deps {
83 if let Some(dep) = value.as_table()
84 && let Some(workspace) = dep.get("workspace")
85 && workspace.as_bool().unwrap_or(false)
86 {
87 dep_names.push(dep_name.clone());
88 }
89 }
90 }
91
92 if cargo_toml.get("workspace").is_some() {
94 let ws_pkg_version = cargo_toml
96 .get("workspace")
97 .and_then(|w| w.get("package"))
98 .and_then(|p| p.get("version"))
99 .and_then(|v| v.as_str())
100 .map(String::from);
101 if ws_pkg_version.is_some() {
102 self.workspace_package_version = ws_pkg_version;
103 self.workspace_root_path = Some(path.to_path_buf());
104 }
105
106 let version = cargo_toml
107 .get("package")
108 .and_then(|p| p.get("version"))
109 .and_then(|v| v.as_str())
110 .map(std::string::ToString::to_string);
111 let name = cargo_toml
112 .get("package")
113 .and_then(|p| p.get("name"))
114 .and_then(|v| v.as_str())
115 .map(std::string::ToString::to_string);
116 let mut project = Project::Workspace(Box::new(RustWorkspace::new(
117 name,
118 version,
119 path.to_path_buf(),
120 relative_path.to_path_buf(),
121 )));
122 for dep_name in &dep_names {
123 project.add_dependency(dep_name);
124 }
125 self.projects.insert(path.to_path_buf(), project);
126
127 let pending = std::mem::take(&mut self.pending_workspace_packages);
129 for p in pending {
130 let mut pkg = RustPackage::new_with_workspace_version(
131 p.name,
132 self.workspace_package_version.clone(),
133 p.abs_path.clone(),
134 p.relative_path,
135 self.workspace_root_path.clone(),
136 );
137 for dep in &p.dependencies {
138 pkg.add_dependency(dep);
139 }
140 self.projects
141 .insert(p.abs_path, Project::Package(Box::new(pkg)));
142 }
143 } else {
144 let inherits_workspace = cargo_toml
146 .get("package")
147 .and_then(|p| p.get("version"))
148 .and_then(|v| v.as_table())
149 .and_then(|t| t.get("workspace"))
150 .and_then(|w| w.as_bool())
151 .unwrap_or(false);
152
153 let name = cargo_toml["package"]["name"]
154 .as_str()
155 .map(std::string::ToString::to_string);
156
157 if inherits_workspace {
158 if self.workspace_package_version.is_some() {
159 let mut pkg = RustPackage::new_with_workspace_version(
161 name,
162 self.workspace_package_version.clone(),
163 path.to_path_buf(),
164 relative_path.to_path_buf(),
165 self.workspace_root_path.clone(),
166 );
167 for dep_name in &dep_names {
168 pkg.add_dependency(dep_name);
169 }
170 self.projects
171 .insert(path.to_path_buf(), Project::Package(Box::new(pkg)));
172 } else {
173 self.pending_workspace_packages
175 .push(PendingWorkspacePackage {
176 name,
177 abs_path: path.to_path_buf(),
178 relative_path: relative_path.to_path_buf(),
179 dependencies: dep_names,
180 });
181 }
182 } else {
183 let version = cargo_toml["package"]["version"]
184 .as_str()
185 .map(std::string::ToString::to_string);
186 let mut project = Project::Package(Box::new(RustPackage::new(
187 name,
188 version,
189 path.to_path_buf(),
190 relative_path.to_path_buf(),
191 )));
192 for dep_name in &dep_names {
193 project.add_dependency(dep_name);
194 }
195 self.projects.insert(path.to_path_buf(), project);
196 }
197 };
198 }
199 Ok(())
200 }
201
202 async fn finalize(&mut self) -> Result<()> {
203 if self.workspace_package_version.is_none()
206 && !self.pending_workspace_packages.is_empty()
207 && let Some(first_pkg) = self.pending_workspace_packages.first()
208 {
209 let rel_component_count = first_pkg.relative_path.components().count();
212 let mut git_root = first_pkg.abs_path.clone();
213 for _ in 0..rel_component_count {
214 git_root.pop();
215 }
216
217 let mut dir = first_pkg.abs_path.parent().and_then(Path::parent);
218 while let Some(parent) = dir {
219 let candidate = parent.join("Cargo.toml");
220 if candidate.is_file()
221 && let Ok(content) = read_to_string(&candidate).await
222 && let Ok(parsed) = toml::from_str::<toml::Value>(&content)
223 && let Some(version) = parsed
224 .get("workspace")
225 .and_then(|w| w.get("package"))
226 .and_then(|p| p.get("version"))
227 .and_then(|v| v.as_str())
228 {
229 self.workspace_package_version = Some(version.to_string());
230 self.workspace_root_path = Some(candidate.clone());
231
232 let ws_name = parsed
234 .get("package")
235 .and_then(|p| p.get("name"))
236 .and_then(|v| v.as_str())
237 .map(String::from);
238 let ws_pkg_version = parsed
239 .get("package")
240 .and_then(|p| p.get("version"))
241 .and_then(|v| v.as_str())
242 .map(String::from);
243 let ws_relative_path = candidate
244 .strip_prefix(&git_root)
245 .unwrap_or(Path::new("Cargo.toml"))
246 .to_path_buf();
247
248 let workspace = RustWorkspace::new(
249 ws_name,
250 ws_pkg_version.or_else(|| self.workspace_package_version.clone()),
252 candidate,
253 ws_relative_path,
254 );
255 self.projects.insert(
256 self.workspace_root_path.clone().unwrap(),
257 Project::Workspace(Box::new(workspace)),
258 );
259 break;
260 }
261 dir = parent.parent();
262 }
263 }
264
265 for pending in self.pending_workspace_packages.drain(..) {
266 let mut pkg = RustPackage::new_with_workspace_version(
267 pending.name,
268 self.workspace_package_version.clone(),
269 pending.abs_path.clone(),
270 pending.relative_path,
271 self.workspace_root_path.clone(),
272 );
273 for dep in &pending.dependencies {
274 pkg.add_dependency(dep);
275 }
276 self.projects
277 .insert(pending.abs_path, Project::Package(Box::new(pkg)));
278 }
279 Ok(())
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use changepacks_core::Project;
287 use std::fs;
288 use tempfile::TempDir;
289
290 #[test]
291 fn test_rust_project_finder_new() {
292 let finder = RustProjectFinder::new();
293 assert_eq!(finder.project_files(), &["Cargo.toml"]);
294 assert_eq!(finder.projects().len(), 0);
295 }
296
297 #[test]
298 fn test_rust_project_finder_default() {
299 let finder = RustProjectFinder::default();
300 assert_eq!(finder.project_files(), &["Cargo.toml"]);
301 assert_eq!(finder.projects().len(), 0);
302 }
303
304 #[tokio::test]
305 async fn test_rust_project_finder_visit_package() {
306 let temp_dir = TempDir::new().unwrap();
307 let cargo_toml = temp_dir.path().join("Cargo.toml");
308 fs::write(
309 &cargo_toml,
310 r#"[package]
311name = "test-package"
312version = "1.0.0"
313"#,
314 )
315 .unwrap();
316
317 let mut finder = RustProjectFinder::new();
318 finder
319 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
320 .await
321 .unwrap();
322
323 let projects = finder.projects();
324 assert_eq!(projects.len(), 1);
325 match projects[0] {
326 Project::Package(pkg) => {
327 assert_eq!(pkg.name(), Some("test-package"));
328 assert_eq!(pkg.version(), Some("1.0.0"));
329 }
330 _ => panic!("Expected Package"),
331 }
332
333 temp_dir.close().unwrap();
334 }
335
336 #[tokio::test]
337 async fn test_rust_project_finder_visit_workspace() {
338 let temp_dir = TempDir::new().unwrap();
339 let cargo_toml = temp_dir.path().join("Cargo.toml");
340 fs::write(
341 &cargo_toml,
342 r#"[workspace]
343members = ["crates/*"]
344
345[package]
346name = "test-workspace"
347version = "1.0.0"
348"#,
349 )
350 .unwrap();
351
352 let mut finder = RustProjectFinder::new();
353 finder
354 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
355 .await
356 .unwrap();
357
358 let projects = finder.projects();
359 assert_eq!(projects.len(), 1);
360 match projects[0] {
361 Project::Workspace(ws) => {
362 assert_eq!(ws.name(), Some("test-workspace"));
363 assert_eq!(ws.version(), Some("1.0.0"));
364 }
365 _ => panic!("Expected Workspace"),
366 }
367
368 temp_dir.close().unwrap();
369 }
370
371 #[tokio::test]
372 async fn test_rust_project_finder_visit_workspace_without_package() {
373 let temp_dir = TempDir::new().unwrap();
374 let cargo_toml = temp_dir.path().join("Cargo.toml");
375 fs::write(
376 &cargo_toml,
377 r#"[workspace]
378members = ["crates/*"]
379"#,
380 )
381 .unwrap();
382
383 let mut finder = RustProjectFinder::new();
384 finder
385 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
386 .await
387 .unwrap();
388
389 let projects = finder.projects();
390 assert_eq!(projects.len(), 1);
391 match projects[0] {
392 Project::Workspace(ws) => {
393 assert_eq!(ws.name(), None);
394 assert_eq!(ws.version(), None);
395 }
396 _ => panic!("Expected Workspace"),
397 }
398
399 temp_dir.close().unwrap();
400 }
401
402 #[tokio::test]
403 async fn test_rust_project_finder_visit_non_cargo_file() {
404 let temp_dir = TempDir::new().unwrap();
405 let other_file = temp_dir.path().join("other.txt");
406 fs::write(&other_file, "some content").unwrap();
407
408 let mut finder = RustProjectFinder::new();
409 finder
410 .visit(&other_file, &PathBuf::from("other.txt"))
411 .await
412 .unwrap();
413
414 assert_eq!(finder.projects().len(), 0);
415
416 temp_dir.close().unwrap();
417 }
418
419 #[tokio::test]
420 async fn test_rust_project_finder_visit_directory() {
421 let temp_dir = TempDir::new().unwrap();
422 let cargo_toml = temp_dir.path().join("Cargo.toml");
423 fs::write(
424 &cargo_toml,
425 r#"[package]
426name = "test-package"
427version = "1.0.0"
428"#,
429 )
430 .unwrap();
431
432 let mut finder = RustProjectFinder::new();
433 finder
435 .visit(temp_dir.path(), &PathBuf::from("."))
436 .await
437 .unwrap();
438
439 assert_eq!(finder.projects().len(), 0);
440
441 temp_dir.close().unwrap();
442 }
443
444 #[tokio::test]
445 async fn test_rust_project_finder_visit_duplicate() {
446 let temp_dir = TempDir::new().unwrap();
447 let cargo_toml = temp_dir.path().join("Cargo.toml");
448 fs::write(
449 &cargo_toml,
450 r#"[package]
451name = "test-package"
452version = "1.0.0"
453"#,
454 )
455 .unwrap();
456
457 let mut finder = RustProjectFinder::new();
458 finder
459 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
460 .await
461 .unwrap();
462
463 assert_eq!(finder.projects().len(), 1);
464
465 finder
467 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
468 .await
469 .unwrap();
470
471 assert_eq!(finder.projects().len(), 1);
472
473 temp_dir.close().unwrap();
474 }
475
476 #[tokio::test]
477 async fn test_rust_project_finder_visit_multiple_packages() {
478 let temp_dir = TempDir::new().unwrap();
479 let cargo_toml1 = temp_dir.path().join("package1").join("Cargo.toml");
480 fs::create_dir_all(cargo_toml1.parent().unwrap()).unwrap();
481 fs::write(
482 &cargo_toml1,
483 r#"[package]
484name = "package1"
485version = "1.0.0"
486"#,
487 )
488 .unwrap();
489
490 let cargo_toml2 = temp_dir.path().join("package2").join("Cargo.toml");
491 fs::create_dir_all(cargo_toml2.parent().unwrap()).unwrap();
492 fs::write(
493 &cargo_toml2,
494 r#"[package]
495name = "package2"
496version = "2.0.0"
497"#,
498 )
499 .unwrap();
500
501 let mut finder = RustProjectFinder::new();
502 finder
503 .visit(&cargo_toml1, &PathBuf::from("package1/Cargo.toml"))
504 .await
505 .unwrap();
506 finder
507 .visit(&cargo_toml2, &PathBuf::from("package2/Cargo.toml"))
508 .await
509 .unwrap();
510
511 let projects = finder.projects();
512 assert_eq!(projects.len(), 2);
513
514 temp_dir.close().unwrap();
515 }
516
517 #[tokio::test]
518 async fn test_rust_project_finder_projects_mut() {
519 let temp_dir = TempDir::new().unwrap();
520 let cargo_toml = temp_dir.path().join("Cargo.toml");
521 fs::write(
522 &cargo_toml,
523 r#"[package]
524name = "test-package"
525version = "1.0.0"
526"#,
527 )
528 .unwrap();
529
530 let mut finder = RustProjectFinder::new();
531 finder
532 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
533 .await
534 .unwrap();
535
536 let mut_projects = finder.projects_mut();
537 assert_eq!(mut_projects.len(), 1);
538
539 temp_dir.close().unwrap();
540 }
541
542 #[tokio::test]
543 async fn test_rust_project_finder_visit_package_with_workspace_dependencies() {
544 let temp_dir = TempDir::new().unwrap();
545 let cargo_toml = temp_dir.path().join("Cargo.toml");
546 fs::write(
547 &cargo_toml,
548 r#"[package]
549name = "test-package"
550version = "1.0.0"
551
552[dependencies]
553core = { workspace = true }
554utils = { workspace = true }
555external = "1.0"
556"#,
557 )
558 .unwrap();
559
560 let mut finder = RustProjectFinder::new();
561 finder
562 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
563 .await
564 .unwrap();
565
566 let projects = finder.projects();
567 assert_eq!(projects.len(), 1);
568 match projects[0] {
569 Project::Package(pkg) => {
570 assert_eq!(pkg.name(), Some("test-package"));
571 let deps = pkg.dependencies();
572 assert_eq!(deps.len(), 2);
573 assert!(deps.contains("core"));
574 assert!(deps.contains("utils"));
575 assert!(!deps.contains("external"));
577 }
578 _ => panic!("Expected Package"),
579 }
580
581 temp_dir.close().unwrap();
582 }
583
584 #[tokio::test]
585 async fn test_rust_project_finder_virtual_workspace_with_workspace_version() {
586 let temp_dir = TempDir::new().unwrap();
588
589 let workspace_toml = temp_dir.path().join("Cargo.toml");
590 fs::write(
591 &workspace_toml,
592 r#"[workspace]
593resolver = "2"
594members = ["crates/*"]
595
596[workspace.package]
597version = "0.1.33"
598edition = "2024"
599"#,
600 )
601 .unwrap();
602
603 let pkg_dir = temp_dir.path().join("crates").join("vespera");
604 fs::create_dir_all(&pkg_dir).unwrap();
605 let pkg_toml = pkg_dir.join("Cargo.toml");
606 fs::write(
607 &pkg_toml,
608 r#"[package]
609name = "vespera"
610version.workspace = true
611edition.workspace = true
612
613[dependencies]
614vespera_core = { workspace = true }
615
616[lints]
617workspace = true
618"#,
619 )
620 .unwrap();
621
622 let mut finder = RustProjectFinder::new();
623 finder
624 .visit(&workspace_toml, &PathBuf::from("Cargo.toml"))
625 .await
626 .unwrap();
627 finder
628 .visit(&pkg_toml, &PathBuf::from("crates/vespera/Cargo.toml"))
629 .await
630 .unwrap();
631 finder.finalize().await.unwrap();
632
633 let projects = finder.projects();
634 assert_eq!(projects.len(), 2);
636
637 let pkg = projects
638 .iter()
639 .find(|p| p.name() == Some("vespera"))
640 .unwrap();
641 assert_eq!(pkg.version(), Some("0.1.33"));
642 }
643
644 #[tokio::test]
645 async fn test_rust_project_finder_visit_package_with_workspace_version() {
646 let temp_dir = TempDir::new().unwrap();
647
648 let workspace_toml = temp_dir.path().join("Cargo.toml");
650 fs::write(
651 &workspace_toml,
652 r#"[workspace]
653members = ["crates/*"]
654
655[workspace.package]
656version = "2.5.0"
657edition = "2024"
658
659[package]
660name = "my-workspace"
661version = "2.5.0"
662"#,
663 )
664 .unwrap();
665
666 let pkg_dir = temp_dir.path().join("crates").join("my-crate");
668 fs::create_dir_all(&pkg_dir).unwrap();
669 let pkg_toml = pkg_dir.join("Cargo.toml");
670 fs::write(
671 &pkg_toml,
672 r#"[package]
673name = "my-crate"
674version.workspace = true
675edition.workspace = true
676"#,
677 )
678 .unwrap();
679
680 let mut finder = RustProjectFinder::new();
681 finder
683 .visit(&workspace_toml, &PathBuf::from("Cargo.toml"))
684 .await
685 .unwrap();
686 finder
687 .visit(&pkg_toml, &PathBuf::from("crates/my-crate/Cargo.toml"))
688 .await
689 .unwrap();
690 finder.finalize().await.unwrap();
691
692 let projects = finder.projects();
693 assert_eq!(projects.len(), 2);
694
695 let pkg = projects
697 .iter()
698 .find(|p| p.name() == Some("my-crate"))
699 .unwrap();
700 assert_eq!(pkg.version(), Some("2.5.0")); }
702
703 #[tokio::test]
704 async fn test_rust_project_finder_visit_package_before_workspace() {
705 let temp_dir = TempDir::new().unwrap();
706
707 let workspace_toml = temp_dir.path().join("Cargo.toml");
709 fs::write(
710 &workspace_toml,
711 r#"[workspace]
712members = ["crates/*"]
713
714[workspace.package]
715version = "3.0.0"
716
717[package]
718name = "my-workspace"
719version = "3.0.0"
720"#,
721 )
722 .unwrap();
723
724 let pkg_dir = temp_dir.path().join("crates").join("my-crate");
726 fs::create_dir_all(&pkg_dir).unwrap();
727 let pkg_toml = pkg_dir.join("Cargo.toml");
728 fs::write(
729 &pkg_toml,
730 r#"[package]
731name = "my-crate"
732version.workspace = true
733"#,
734 )
735 .unwrap();
736
737 let mut finder = RustProjectFinder::new();
738 finder
740 .visit(&pkg_toml, &PathBuf::from("crates/my-crate/Cargo.toml"))
741 .await
742 .unwrap();
743 finder
744 .visit(&workspace_toml, &PathBuf::from("Cargo.toml"))
745 .await
746 .unwrap();
747 finder.finalize().await.unwrap();
748
749 let projects = finder.projects();
750 assert_eq!(projects.len(), 2);
751
752 let pkg = projects
753 .iter()
754 .find(|p| p.name() == Some("my-crate"))
755 .unwrap();
756 assert_eq!(pkg.version(), Some("3.0.0")); }
758
759 #[tokio::test]
760 async fn test_rust_project_finder_workspace_ignored_by_config() {
761 let temp_dir = TempDir::new().unwrap();
763
764 let workspace_toml = temp_dir.path().join("Cargo.toml");
766 fs::write(
767 &workspace_toml,
768 r#"[workspace]
769resolver = "2"
770members = ["crates/*"]
771
772[workspace.package]
773version = "0.1.33"
774edition = "2024"
775"#,
776 )
777 .unwrap();
778
779 for name in ["vespera", "vespera_core"] {
781 let pkg_dir = temp_dir.path().join("crates").join(name);
782 fs::create_dir_all(&pkg_dir).unwrap();
783 fs::write(
784 pkg_dir.join("Cargo.toml"),
785 format!(
786 r#"[package]
787name = "{name}"
788version.workspace = true
789edition.workspace = true
790
791[lints]
792workspace = true
793"#
794 ),
795 )
796 .unwrap();
797 }
798
799 let mut finder = RustProjectFinder::new();
800 for name in ["vespera", "vespera_core"] {
802 let pkg_toml = temp_dir.path().join("crates").join(name).join("Cargo.toml");
803 finder
804 .visit(
805 &pkg_toml,
806 &PathBuf::from(format!("crates/{name}/Cargo.toml")),
807 )
808 .await
809 .unwrap();
810 }
811 finder.finalize().await.unwrap();
813
814 let projects = finder.projects();
815 assert_eq!(projects.len(), 3);
817
818 for name in ["vespera", "vespera_core"] {
819 let pkg = projects.iter().find(|p| p.name() == Some(name)).unwrap();
820 assert_eq!(
821 pkg.version(),
822 Some("0.1.33"),
823 "{name} should inherit workspace version"
824 );
825 }
826
827 let ws = projects
829 .iter()
830 .find(|p| matches!(p, Project::Workspace(_)))
831 .expect("synthetic workspace should be created");
832 assert_eq!(ws.version(), Some("0.1.33"));
833 assert_eq!(ws.relative_path(), Path::new("Cargo.toml"));
834 }
835
836 #[tokio::test]
837 async fn test_rust_project_finder_finalize_discovers_workspace_with_package_section() {
838 let temp_dir = TempDir::new().unwrap();
841
842 let workspace_toml = temp_dir.path().join("Cargo.toml");
843 fs::write(
844 &workspace_toml,
845 r#"[workspace]
846resolver = "2"
847members = ["crates/*"]
848
849[workspace.package]
850version = "0.2.0"
851
852[package]
853name = "my-workspace-root"
854version = "0.2.0"
855"#,
856 )
857 .unwrap();
858
859 let pkg_dir = temp_dir.path().join("crates").join("my-crate");
860 fs::create_dir_all(&pkg_dir).unwrap();
861 fs::write(
862 pkg_dir.join("Cargo.toml"),
863 r#"[package]
864name = "my-crate"
865version.workspace = true
866"#,
867 )
868 .unwrap();
869
870 let mut finder = RustProjectFinder::new();
871 let pkg_toml = pkg_dir.join("Cargo.toml");
873 finder
874 .visit(&pkg_toml, &PathBuf::from("crates/my-crate/Cargo.toml"))
875 .await
876 .unwrap();
877 finder.finalize().await.unwrap();
878
879 let projects = finder.projects();
880 assert_eq!(projects.len(), 2);
881
882 let ws = projects
883 .iter()
884 .find(|p| matches!(p, Project::Workspace(_)))
885 .unwrap();
886 assert_eq!(ws.name(), Some("my-workspace-root"));
887 assert_eq!(ws.version(), Some("0.2.0"));
888
889 let pkg = projects
890 .iter()
891 .find(|p| p.name() == Some("my-crate"))
892 .unwrap();
893 assert_eq!(pkg.version(), Some("0.2.0"));
894 }
895}