1use std::{
2 cmp::Ordering,
3 collections::HashSet,
4 fmt::{Debug, Display},
5 path::Path,
6};
7
8use anyhow::Result;
9use colored::Colorize;
10
11use crate::{config::Config, package::Package, update_type::UpdateType, workspace::Workspace};
12
13#[derive(Debug)]
19pub enum Project {
20 Workspace(Box<dyn Workspace>),
22 Package(Box<dyn Package>),
24}
25
26impl Project {
27 #[must_use]
28 pub fn name(&self) -> Option<&str> {
29 match self {
30 Self::Workspace(workspace) => workspace.name(),
31 Self::Package(package) => package.name(),
32 }
33 }
34
35 #[must_use]
36 pub fn version(&self) -> Option<&str> {
37 match self {
38 Self::Workspace(workspace) => workspace.version(),
39 Self::Package(package) => package.version(),
40 }
41 }
42 #[must_use]
43 pub fn path(&self) -> &Path {
44 match self {
45 Self::Workspace(workspace) => workspace.path(),
46 Self::Package(package) => package.path(),
47 }
48 }
49
50 #[must_use]
51 pub fn relative_path(&self) -> &Path {
52 match self {
53 Self::Workspace(workspace) => workspace.relative_path(),
54 Self::Package(package) => package.relative_path(),
55 }
56 }
57
58 pub async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
61 match self {
62 Self::Workspace(workspace) => workspace.update_version(update_type).await?,
63 Self::Package(package) => package.update_version(update_type).await?,
64 }
65 Ok(())
66 }
67
68 pub fn check_changed(&mut self, path: &Path) -> Result<()> {
71 match self {
72 Self::Workspace(workspace) => workspace.check_changed(path)?,
73 Self::Package(package) => package.check_changed(path)?,
74 }
75 Ok(())
76 }
77
78 #[must_use]
79 pub fn is_changed(&self) -> bool {
80 match self {
81 Self::Workspace(workspace) => workspace.is_changed(),
82 Self::Package(package) => package.is_changed(),
83 }
84 }
85
86 #[must_use]
87 pub fn dependencies(&self) -> &HashSet<String> {
88 match self {
89 Self::Workspace(workspace) => workspace.dependencies(),
90 Self::Package(package) => package.dependencies(),
91 }
92 }
93
94 pub fn add_dependency(&mut self, dependency: &str) {
95 match self {
96 Self::Workspace(workspace) => workspace.add_dependency(dependency),
97 Self::Package(package) => package.add_dependency(dependency),
98 }
99 }
100
101 pub fn set_name(&mut self, name: String) {
102 match self {
103 Self::Workspace(workspace) => workspace.set_name(name),
104 Self::Package(package) => package.set_name(name),
105 }
106 }
107
108 #[must_use]
109 pub fn language(&self) -> crate::Language {
110 match self {
111 Self::Workspace(workspace) => workspace.language(),
112 Self::Package(package) => package.language(),
113 }
114 }
115
116 pub async fn publish(&self, config: &Config) -> Result<crate::publish::PublishOutput> {
119 match self {
120 Self::Workspace(workspace) => workspace.publish(config).await,
121 Self::Package(package) => package.publish(config).await,
122 }
123 }
124
125 pub async fn dry_run_publish(
133 &self,
134 config: &Config,
135 ) -> Result<Option<crate::publish::PublishOutput>> {
136 match self {
137 Self::Workspace(workspace) => workspace.dry_run_publish(config).await,
138 Self::Package(package) => package.dry_run_publish(config).await,
139 }
140 }
141}
142
143impl PartialEq for Project {
144 fn eq(&self, other: &Self) -> bool {
145 self.cmp(other) == Ordering::Equal
146 }
147}
148
149impl Eq for Project {}
150
151impl PartialOrd for Project {
152 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
153 Some(self.cmp(other))
154 }
155}
156
157impl Ord for Project {
158 fn cmp(&self, other: &Self) -> Ordering {
159 match (self, other) {
160 (Self::Workspace(_), Self::Package(_)) => Ordering::Less,
161 (Self::Package(_), Self::Workspace(_)) => Ordering::Greater,
162 (Self::Workspace(w1), Self::Workspace(w2)) => {
163 let lang_ord = w1.language().cmp(&w2.language());
164 if lang_ord != Ordering::Equal {
165 return lang_ord;
166 }
167
168 let name1 = w1.name();
169 let name2 = w2.name();
170
171 match (name1, name2) {
172 (Some(n1), Some(n2)) => n1.cmp(n2),
173 (Some(_), None) => Ordering::Less,
174 (None, Some(_)) => Ordering::Greater,
175 (None, None) => {
176 let v1 = w1.version().unwrap_or("");
177 let v2 = w2.version().unwrap_or("");
178 v1.cmp(v2)
179 }
180 }
181 }
182 (Self::Package(p1), Self::Package(p2)) => {
183 let lang_ord = p1.language().cmp(&p2.language());
184 if lang_ord != Ordering::Equal {
185 return lang_ord;
186 }
187 match (p1.name(), p2.name()) {
188 (Some(n1), Some(n2)) => n1.cmp(n2),
189 (Some(_), None) => Ordering::Less,
190 (None, Some(_)) => Ordering::Greater,
191 (None, None) => Ordering::Equal,
192 }
193 }
194 }
195 }
196}
197
198impl Display for Project {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 match self {
201 Self::Workspace(workspace) => {
202 write!(
203 f,
204 "{} {} {} {} {}",
205 format!("[Workspace - {}]", workspace.language())
206 .bright_blue()
207 .bold(),
208 workspace.name().unwrap_or("noname").bright_white().bold(),
209 format!(
210 "({})",
211 workspace
212 .version()
213 .map_or("unknown".to_string(), |v| format!("v{v}")),
214 )
215 .bright_green(),
216 "-".bright_cyan(),
217 workspace
218 .relative_path()
219 .display()
220 .to_string()
221 .bright_black()
222 )
223 }
224 Self::Package(package) => {
225 write!(
226 f,
227 "{} {} {} {} {}",
228 format!("[{}]", package.language()).bright_blue().bold(),
229 package.name().unwrap_or("noname").bright_white().bold(),
230 format!(
231 "({})",
232 package
233 .version()
234 .map_or("unknown".to_string(), |v| format!("v{v}"))
235 )
236 .bright_green(),
237 "-".bright_cyan(),
238 package.relative_path().display().to_string().bright_black()
239 )
240 }
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::Language;
249 use async_trait::async_trait;
250 use std::path::PathBuf;
251
252 #[derive(Debug)]
253 struct MockWorkspace {
254 name: Option<String>,
255 version: Option<String>,
256 path: PathBuf,
257 relative_path: PathBuf,
258 language: Language,
259 dependencies: HashSet<String>,
260 changed: bool,
261 }
262
263 impl MockWorkspace {
264 fn new(name: Option<&str>, version: Option<&str>, language: Language) -> Self {
265 Self {
266 name: name.map(String::from),
267 version: version.map(String::from),
268 path: PathBuf::from("/test/package.json"),
269 relative_path: PathBuf::from("package.json"),
270 language,
271 dependencies: HashSet::new(),
272 changed: false,
273 }
274 }
275 }
276
277 #[async_trait]
278 impl Workspace for MockWorkspace {
279 fn name(&self) -> Option<&str> {
280 self.name.as_deref()
281 }
282 fn path(&self) -> &Path {
283 &self.path
284 }
285 fn relative_path(&self) -> &Path {
286 &self.relative_path
287 }
288 fn version(&self) -> Option<&str> {
289 self.version.as_deref()
290 }
291 async fn update_version(&mut self, _update_type: UpdateType) -> Result<()> {
292 Ok(())
293 }
294 fn language(&self) -> Language {
295 self.language
296 }
297 fn dependencies(&self) -> &HashSet<String> {
298 &self.dependencies
299 }
300 fn add_dependency(&mut self, dependency: &str) {
301 self.dependencies.insert(dependency.to_string());
302 }
303 fn is_changed(&self) -> bool {
304 self.changed
305 }
306 fn set_changed(&mut self, changed: bool) {
307 self.changed = changed;
308 }
309 fn default_publish_command(&self) -> String {
310 "echo publish".to_string()
311 }
312 fn default_dry_run_publish_command(&self) -> Option<String> {
313 Some("echo publish --dry-run".to_string())
314 }
315 }
316
317 #[derive(Debug)]
318 struct MockPackage {
319 name: Option<String>,
320 version: Option<String>,
321 path: PathBuf,
322 relative_path: PathBuf,
323 language: Language,
324 dependencies: HashSet<String>,
325 changed: bool,
326 }
327
328 impl MockPackage {
329 fn new(name: Option<&str>, version: Option<&str>, language: Language) -> Self {
330 Self {
331 name: name.map(String::from),
332 version: version.map(String::from),
333 path: PathBuf::from("/test/Cargo.toml"),
334 relative_path: PathBuf::from("Cargo.toml"),
335 language,
336 dependencies: HashSet::new(),
337 changed: false,
338 }
339 }
340 }
341
342 #[async_trait]
343 impl Package for MockPackage {
344 fn name(&self) -> Option<&str> {
345 self.name.as_deref()
346 }
347 fn path(&self) -> &Path {
348 &self.path
349 }
350 fn relative_path(&self) -> &Path {
351 &self.relative_path
352 }
353 fn version(&self) -> Option<&str> {
354 self.version.as_deref()
355 }
356 async fn update_version(&mut self, _update_type: UpdateType) -> Result<()> {
357 Ok(())
358 }
359 fn language(&self) -> Language {
360 self.language
361 }
362 fn dependencies(&self) -> &HashSet<String> {
363 &self.dependencies
364 }
365 fn add_dependency(&mut self, dependency: &str) {
366 self.dependencies.insert(dependency.to_string());
367 }
368 fn is_changed(&self) -> bool {
369 self.changed
370 }
371 fn set_changed(&mut self, changed: bool) {
372 self.changed = changed;
373 }
374 fn default_publish_command(&self) -> String {
375 "echo publish".to_string()
376 }
377 fn default_dry_run_publish_command(&self) -> Option<String> {
378 Some("echo publish --dry-run".to_string())
379 }
380 }
381
382 #[test]
383 fn test_project_workspace_name() {
384 let workspace = MockWorkspace::new(Some("test-ws"), Some("1.0.0"), Language::Node);
385 let project = Project::Workspace(Box::new(workspace));
386 assert_eq!(project.name(), Some("test-ws"));
387 }
388
389 #[test]
390 fn test_project_package_name() {
391 let package = MockPackage::new(Some("test-pkg"), Some("1.0.0"), Language::Rust);
392 let project = Project::Package(Box::new(package));
393 assert_eq!(project.name(), Some("test-pkg"));
394 }
395
396 #[test]
397 fn test_project_workspace_version() {
398 let workspace = MockWorkspace::new(Some("test"), Some("2.0.0"), Language::Node);
399 let project = Project::Workspace(Box::new(workspace));
400 assert_eq!(project.version(), Some("2.0.0"));
401 }
402
403 #[test]
404 fn test_project_package_version() {
405 let package = MockPackage::new(Some("test"), Some("3.0.0"), Language::Rust);
406 let project = Project::Package(Box::new(package));
407 assert_eq!(project.version(), Some("3.0.0"));
408 }
409
410 #[test]
411 fn test_project_workspace_path() {
412 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
413 let project = Project::Workspace(Box::new(workspace));
414 assert_eq!(project.path(), Path::new("/test/package.json"));
415 }
416
417 #[test]
418 fn test_project_package_path() {
419 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
420 let project = Project::Package(Box::new(package));
421 assert_eq!(project.path(), Path::new("/test/Cargo.toml"));
422 }
423
424 #[test]
425 fn test_project_workspace_relative_path() {
426 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
427 let project = Project::Workspace(Box::new(workspace));
428 assert_eq!(project.relative_path(), Path::new("package.json"));
429 }
430
431 #[test]
432 fn test_project_package_relative_path() {
433 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
434 let project = Project::Package(Box::new(package));
435 assert_eq!(project.relative_path(), Path::new("Cargo.toml"));
436 }
437
438 #[tokio::test]
439 async fn test_project_workspace_update_version() {
440 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
441 let mut project = Project::Workspace(Box::new(workspace));
442 let result = project.update_version(UpdateType::Minor).await;
443 assert!(result.is_ok());
444 }
445
446 #[tokio::test]
447 async fn test_project_package_update_version() {
448 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
449 let mut project = Project::Package(Box::new(package));
450 let result = project.update_version(UpdateType::Patch).await;
451 assert!(result.is_ok());
452 }
453
454 #[test]
455 fn test_project_workspace_check_changed() {
456 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
457 let mut project = Project::Workspace(Box::new(workspace));
458 let result = project.check_changed(Path::new("/test/src/index.js"));
459 assert!(result.is_ok());
460 }
461
462 #[test]
463 fn test_project_package_check_changed() {
464 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
465 let mut project = Project::Package(Box::new(package));
466 let result = project.check_changed(Path::new("/test/src/main.rs"));
467 assert!(result.is_ok());
468 }
469
470 #[test]
471 fn test_project_workspace_is_changed() {
472 let mut workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
473 workspace.changed = true;
474 let project = Project::Workspace(Box::new(workspace));
475 assert!(project.is_changed());
476 }
477
478 #[test]
479 fn test_project_package_is_changed() {
480 let mut package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
481 package.changed = true;
482 let project = Project::Package(Box::new(package));
483 assert!(project.is_changed());
484 }
485
486 #[test]
487 fn test_project_workspace_dependencies() {
488 let mut workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
489 workspace.dependencies.insert("dep1".to_string());
490 let project = Project::Workspace(Box::new(workspace));
491 assert!(project.dependencies().contains("dep1"));
492 }
493
494 #[test]
495 fn test_project_package_dependencies() {
496 let mut package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
497 package.dependencies.insert("dep2".to_string());
498 let project = Project::Package(Box::new(package));
499 assert!(project.dependencies().contains("dep2"));
500 }
501
502 #[test]
503 fn test_project_workspace_add_dependency() {
504 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
505 let mut project = Project::Workspace(Box::new(workspace));
506 project.add_dependency("new-dep");
507 assert!(project.dependencies().contains("new-dep"));
508 }
509
510 #[test]
511 fn test_project_package_add_dependency() {
512 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
513 let mut project = Project::Package(Box::new(package));
514 project.add_dependency("new-dep");
515 assert!(project.dependencies().contains("new-dep"));
516 }
517
518 #[test]
519 fn test_project_workspace_language() {
520 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Python);
521 let project = Project::Workspace(Box::new(workspace));
522 assert!(matches!(project.language(), Language::Python));
523 }
524
525 #[test]
526 fn test_project_package_language() {
527 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Dart);
528 let project = Project::Package(Box::new(package));
529 assert!(matches!(project.language(), Language::Dart));
530 }
531
532 #[tokio::test]
533 async fn test_project_workspace_publish() {
534 let temp_dir = std::env::temp_dir();
535 let mut workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
536 workspace.path = temp_dir.join("package.json");
537 let project = Project::Workspace(Box::new(workspace));
538 let config = Config::default();
539 let output = project.publish(&config).await.unwrap();
540 assert!(output.success);
541 }
542
543 #[tokio::test]
544 async fn test_project_package_publish() {
545 let temp_dir = std::env::temp_dir();
546 let mut package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
547 package.path = temp_dir.join("Cargo.toml");
548 let project = Project::Package(Box::new(package));
549 let config = Config::default();
550 let output = project.publish(&config).await.unwrap();
551 assert!(output.success);
552 }
553
554 #[tokio::test]
555 async fn test_project_workspace_dry_run_publish() {
556 let temp_dir = std::env::temp_dir();
557 let mut workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
558 workspace.path = temp_dir.join("package.json");
559 let project = Project::Workspace(Box::new(workspace));
560 let config = Config::default();
561
562 let output = project.dry_run_publish(&config).await.unwrap();
566 assert!(output.is_some());
567 let output = output.unwrap();
568 assert!(output.success);
569 assert!(output.stdout.contains("publish"));
570 }
571
572 #[tokio::test]
573 async fn test_project_package_dry_run_publish() {
574 let temp_dir = std::env::temp_dir();
575 let mut package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
576 package.path = temp_dir.join("Cargo.toml");
577 let project = Project::Package(Box::new(package));
578 let config = Config::default();
579
580 let output = project.dry_run_publish(&config).await.unwrap();
581 assert!(output.is_some());
582 assert!(output.unwrap().success);
583 }
584
585 #[tokio::test]
586 async fn test_project_package_dry_run_publish_propagates_language_override() {
587 let temp_dir = std::env::temp_dir();
588 let mut package = MockPackage::new(Some("test"), Some("1.0.0"), Language::CSharp);
589 package.path = temp_dir.join("Sample.csproj");
590 let project = Project::Package(Box::new(package));
591 let mut publish_dry_run = std::collections::HashMap::new();
592 publish_dry_run.insert("csharp".to_string(), "echo dry-csharp".to_string());
593 let config = Config {
594 publish_dry_run,
595 ..Config::default()
596 };
597
598 let output = project.dry_run_publish(&config).await.unwrap();
602 assert!(output.is_some());
603 assert!(output.unwrap().success);
604 }
605
606 #[test]
607 fn test_project_eq_same_workspace() {
608 let w1 = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
609 let w2 = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
610 let p1 = Project::Workspace(Box::new(w1));
611 let p2 = Project::Workspace(Box::new(w2));
612 assert_eq!(p1, p2);
613 }
614
615 #[test]
616 fn test_project_partial_ord() {
617 let w1 = MockWorkspace::new(Some("a"), Some("1.0.0"), Language::Node);
618 let w2 = MockWorkspace::new(Some("b"), Some("1.0.0"), Language::Node);
619 let p1 = Project::Workspace(Box::new(w1));
620 let p2 = Project::Workspace(Box::new(w2));
621 assert!(p1.partial_cmp(&p2).is_some());
622 }
623
624 #[test]
625 fn test_project_ord_workspace_before_package() {
626 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
627 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
628 let p1 = Project::Workspace(Box::new(workspace));
629 let p2 = Project::Package(Box::new(package));
630 assert!(p1 < p2);
631 }
632
633 #[test]
634 fn test_project_ord_package_after_workspace() {
635 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
636 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
637 let p1 = Project::Package(Box::new(package));
638 let p2 = Project::Workspace(Box::new(workspace));
639 assert!(p1 > p2);
640 }
641
642 #[test]
643 fn test_project_ord_workspaces_by_language() {
644 let w1 = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
645 let w2 = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Python);
646 let p1 = Project::Workspace(Box::new(w1));
647 let p2 = Project::Workspace(Box::new(w2));
648 assert_ne!(p1.cmp(&p2), Ordering::Equal);
649 }
650
651 #[test]
652 fn test_project_ord_workspaces_by_name() {
653 let w1 = MockWorkspace::new(Some("aaa"), Some("1.0.0"), Language::Node);
654 let w2 = MockWorkspace::new(Some("bbb"), Some("1.0.0"), Language::Node);
655 let p1 = Project::Workspace(Box::new(w1));
656 let p2 = Project::Workspace(Box::new(w2));
657 assert!(p1 < p2);
658 }
659
660 #[test]
661 fn test_project_ord_workspaces_name_some_vs_none() {
662 let w1 = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
663 let w2 = MockWorkspace::new(None, Some("1.0.0"), Language::Node);
664 let p1 = Project::Workspace(Box::new(w1));
665 let p2 = Project::Workspace(Box::new(w2));
666 assert!(p1 < p2);
667 }
668
669 #[test]
670 fn test_project_ord_workspaces_name_none_vs_some() {
671 let w1 = MockWorkspace::new(None, Some("1.0.0"), Language::Node);
672 let w2 = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
673 let p1 = Project::Workspace(Box::new(w1));
674 let p2 = Project::Workspace(Box::new(w2));
675 assert!(p1 > p2);
676 }
677
678 #[test]
679 fn test_project_ord_workspaces_both_none_names() {
680 let w1 = MockWorkspace::new(None, Some("1.0.0"), Language::Node);
681 let w2 = MockWorkspace::new(None, Some("2.0.0"), Language::Node);
682 let p1 = Project::Workspace(Box::new(w1));
683 let p2 = Project::Workspace(Box::new(w2));
684 assert!(p1 < p2);
685 }
686
687 #[test]
688 fn test_project_ord_packages_by_language() {
689 let pkg1 = MockPackage::new(Some("test"), Some("1.0.0"), Language::Node);
690 let pkg2 = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
691 let p1 = Project::Package(Box::new(pkg1));
692 let p2 = Project::Package(Box::new(pkg2));
693 assert_ne!(p1.cmp(&p2), Ordering::Equal);
694 }
695
696 #[test]
697 fn test_project_ord_packages_by_name() {
698 let pkg1 = MockPackage::new(Some("aaa"), Some("1.0.0"), Language::Rust);
699 let pkg2 = MockPackage::new(Some("bbb"), Some("1.0.0"), Language::Rust);
700 let p1 = Project::Package(Box::new(pkg1));
701 let p2 = Project::Package(Box::new(pkg2));
702 assert!(p1 < p2);
703 }
704
705 #[test]
706 fn test_project_ord_packages_name_some_vs_none() {
707 let pkg1 = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
708 let pkg2 = MockPackage::new(None, Some("1.0.0"), Language::Rust);
709 let p1 = Project::Package(Box::new(pkg1));
710 let p2 = Project::Package(Box::new(pkg2));
711 assert!(p1 < p2);
712 }
713
714 #[test]
715 fn test_project_ord_packages_name_none_vs_some() {
716 let pkg1 = MockPackage::new(None, Some("1.0.0"), Language::Rust);
717 let pkg2 = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
718 let p1 = Project::Package(Box::new(pkg1));
719 let p2 = Project::Package(Box::new(pkg2));
720 assert!(p1 > p2);
721 }
722
723 #[test]
724 fn test_project_ord_packages_both_none_names() {
725 let pkg1 = MockPackage::new(None, Some("1.0.0"), Language::Rust);
726 let pkg2 = MockPackage::new(None, Some("1.0.0"), Language::Rust);
727 let p1 = Project::Package(Box::new(pkg1));
728 let p2 = Project::Package(Box::new(pkg2));
729 assert_eq!(p1.cmp(&p2), Ordering::Equal);
730 }
731
732 #[test]
733 fn test_project_display_workspace() {
734 let workspace = MockWorkspace::new(Some("my-workspace"), Some("1.0.0"), Language::Node);
735 let project = Project::Workspace(Box::new(workspace));
736 let display = format!("{}", project);
737 assert!(display.contains("Workspace"));
738 assert!(display.contains("my-workspace"));
739 assert!(display.contains("v1.0.0"));
740 }
741
742 #[test]
743 fn test_project_display_workspace_no_name() {
744 let workspace = MockWorkspace::new(None, Some("1.0.0"), Language::Node);
745 let project = Project::Workspace(Box::new(workspace));
746 let display = format!("{}", project);
747 assert!(display.contains("noname"));
748 }
749
750 #[test]
751 fn test_project_display_workspace_no_version() {
752 let workspace = MockWorkspace::new(Some("test"), None, Language::Node);
753 let project = Project::Workspace(Box::new(workspace));
754 let display = format!("{}", project);
755 assert!(display.contains("unknown"));
756 }
757
758 #[test]
759 fn test_project_display_package() {
760 let package = MockPackage::new(Some("my-package"), Some("2.0.0"), Language::Rust);
761 let project = Project::Package(Box::new(package));
762 let display = format!("{}", project);
763 assert!(display.contains("my-package"));
764 assert!(display.contains("v2.0.0"));
765 }
766
767 #[test]
768 fn test_project_display_package_no_name() {
769 let package = MockPackage::new(None, Some("1.0.0"), Language::Rust);
770 let project = Project::Package(Box::new(package));
771 let display = format!("{}", project);
772 assert!(display.contains("noname"));
773 }
774
775 #[test]
776 fn test_project_display_package_no_version() {
777 let package = MockPackage::new(Some("test"), None, Language::Rust);
778 let project = Project::Package(Box::new(package));
779 let display = format!("{}", project);
780 assert!(display.contains("unknown"));
781 }
782
783 #[test]
784 fn test_project_sort_stability() {
785 let make_projects = || {
786 vec![
787 Project::Package(Box::new(MockPackage::new(
788 Some("charlie"),
789 Some("1.0.0"),
790 Language::Rust,
791 ))),
792 Project::Workspace(Box::new(MockWorkspace::new(
793 Some("alpha"),
794 Some("2.0.0"),
795 Language::Node,
796 ))),
797 Project::Package(Box::new(MockPackage::new(
798 Some("bravo"),
799 Some("0.1.0"),
800 Language::Node,
801 ))),
802 Project::Workspace(Box::new(MockWorkspace::new(
803 Some("delta"),
804 Some("3.0.0"),
805 Language::Python,
806 ))),
807 Project::Package(Box::new(MockPackage::new(
808 Some("echo"),
809 Some("1.0.0"),
810 Language::Dart,
811 ))),
812 ]
813 };
814
815 let mut first = make_projects();
816 first.sort();
817 let first_order: Vec<Option<&str>> = first.iter().map(|p| p.name()).collect();
818
819 let mut second = make_projects();
820 second.sort();
821 let second_order: Vec<Option<&str>> = second.iter().map(|p| p.name()).collect();
822
823 let mut third = make_projects();
824 third.sort();
825 let third_order: Vec<Option<&str>> = third.iter().map(|p| p.name()).collect();
826
827 assert_eq!(first_order, second_order);
828 assert_eq!(second_order, third_order);
829 }
830
831 #[test]
832 fn test_project_sort_mixed() {
833 let mut projects = [
834 Project::Package(Box::new(MockPackage::new(
835 Some("pkg-a"),
836 Some("1.0.0"),
837 Language::Node,
838 ))),
839 Project::Workspace(Box::new(MockWorkspace::new(
840 Some("ws-b"),
841 Some("1.0.0"),
842 Language::Node,
843 ))),
844 Project::Package(Box::new(MockPackage::new(
845 Some("pkg-c"),
846 Some("1.0.0"),
847 Language::Rust,
848 ))),
849 Project::Workspace(Box::new(MockWorkspace::new(
850 Some("ws-d"),
851 Some("1.0.0"),
852 Language::Rust,
853 ))),
854 ];
855 projects.sort();
856
857 let workspace_count = projects
859 .iter()
860 .take_while(|p| matches!(p, Project::Workspace(_)))
861 .count();
862 assert_eq!(workspace_count, 2);
863
864 let package_count = projects
865 .iter()
866 .skip(workspace_count)
867 .filter(|p| matches!(p, Project::Package(_)))
868 .count();
869 assert_eq!(package_count, 2);
870 }
871
872 #[test]
873 fn test_project_set_name_workspace() {
874 let workspace = MockWorkspace::new(Some("test"), Some("1.0.0"), Language::Node);
875 let mut project = Project::Workspace(Box::new(workspace));
876 project.set_name("new-name".to_string());
877 assert_eq!(project.name(), Some("test"));
879 }
880
881 #[test]
882 fn test_project_set_name_package() {
883 let package = MockPackage::new(Some("test"), Some("1.0.0"), Language::Rust);
884 let mut project = Project::Package(Box::new(package));
885 project.set_name("new-name".to_string());
886 assert_eq!(project.name(), Some("test"));
888 }
889
890 #[test]
891 fn test_project_cmp_is_consistent_with_eq() {
892 let w1 = MockWorkspace::new(Some("same"), Some("1.0.0"), Language::Node);
894 let w2 = MockWorkspace::new(Some("same"), Some("1.0.0"), Language::Node);
895 let p1 = Project::Workspace(Box::new(w1));
896 let p2 = Project::Workspace(Box::new(w2));
897 assert_eq!(p1, p2);
898 assert_eq!(p1.cmp(&p2), Ordering::Equal);
899
900 let pkg1 = MockPackage::new(Some("same"), Some("1.0.0"), Language::Rust);
902 let pkg2 = MockPackage::new(Some("same"), Some("1.0.0"), Language::Rust);
903 let pp1 = Project::Package(Box::new(pkg1));
904 let pp2 = Project::Package(Box::new(pkg2));
905 assert_eq!(pp1, pp2);
906 assert_eq!(pp1.cmp(&pp2), Ordering::Equal);
907 }
908}