1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use changepacks_core::{Project, ProjectFinder};
4use regex::Regex;
5use std::{
6 collections::HashMap,
7 path::{Path, PathBuf},
8 process::Stdio,
9};
10use tokio::process::Command;
11
12use crate::{package::GradlePackage, workspace::GradleWorkspace};
13
14#[derive(Debug)]
15pub struct GradleProjectFinder {
16 projects: HashMap<PathBuf, Project>,
17 project_files: Vec<&'static str>,
18}
19
20impl Default for GradleProjectFinder {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl GradleProjectFinder {
27 #[must_use]
28 pub fn new() -> Self {
29 Self {
30 projects: HashMap::new(),
31 project_files: vec!["build.gradle.kts", "build.gradle"],
32 }
33 }
34}
35
36#[derive(Debug, Default)]
38struct GradleProperties {
39 name: Option<String>,
40 version: Option<String>,
41 has_subprojects: bool,
42}
43
44fn which_java() -> Option<PathBuf> {
46 let path_var = std::env::var_os("PATH")?;
47 for dir in std::env::split_paths(&path_var) {
48 let candidate = if cfg!(windows) {
49 dir.join("java.exe")
50 } else {
51 dir.join("java")
52 };
53 if candidate.is_file() {
54 return Some(candidate);
55 }
56 }
57 None
58}
59
60fn find_gradlew(start_dir: &Path) -> Option<(PathBuf, PathBuf)> {
68 let gradlew_name = if cfg!(windows) {
69 "gradlew.bat"
70 } else {
71 "gradlew"
72 };
73
74 let mut current = start_dir.to_path_buf();
75 loop {
76 let gradlew = current.join(gradlew_name);
77 if gradlew.exists() {
78 return Some((gradlew, current));
79 }
80 if !current.pop() {
81 return None;
82 }
83 }
84}
85
86async fn get_gradle_properties(project_dir: &Path) -> Result<GradleProperties> {
94 let (gradlew, gradlew_dir) = find_gradlew(project_dir).context(
95 "Gradle wrapper (gradlew) not found. \
96 Ensure the project root contains gradlew or gradlew.bat.",
97 )?;
98
99 anyhow::ensure!(
102 std::env::var_os("JAVA_HOME").is_some() || which_java().is_some(),
103 "Java is required for Gradle projects but JAVA_HOME is not set and 'java' was not found on PATH.\n\
104 Please set the JAVA_HOME environment variable or add java to your PATH."
105 );
106
107 let args: Vec<String> = if gradlew_dir == project_dir {
109 vec!["properties".to_string(), "-q".to_string()]
111 } else {
112 let relative = project_dir
114 .strip_prefix(&gradlew_dir)
115 .context("Failed to compute subproject path")?;
116 let gradle_path = relative
117 .components()
118 .filter_map(|c| c.as_os_str().to_str())
119 .collect::<Vec<_>>()
120 .join(":");
121 vec![format!(":{gradle_path}:properties"), "-q".to_string()]
122 };
123
124 let output = Command::new(&gradlew)
127 .args(&args)
128 .current_dir(&gradlew_dir)
129 .stdout(Stdio::piped())
130 .stderr(Stdio::null())
131 .output()
132 .await?;
133
134 if !output.status.success() {
135 return Ok(GradleProperties::default());
136 }
137
138 let stdout = String::from_utf8_lossy(&output.stdout);
139 let mut props = GradleProperties::default();
140
141 let name_pattern = Regex::new(r"(?m)^name:\s*(.+)$").context("regex")?;
144 let version_pattern = Regex::new(r"(?m)^version:\s*(.+)$").context("regex")?;
145 let subprojects_pattern = Regex::new(r"(?m)^subprojects:\s*(.+)$").context("regex")?;
146
147 if let Some(caps) = name_pattern.captures(&stdout) {
148 let name = caps.get(1).map(|m| m.as_str().trim().to_string());
149 if name.as_deref() != Some("unspecified") {
150 props.name = name;
151 }
152 }
153
154 if let Some(caps) = version_pattern.captures(&stdout) {
155 let version = caps.get(1).map(|m| m.as_str().trim().to_string());
156 if version.as_deref() != Some("unspecified") {
157 props.version = version;
158 }
159 }
160
161 if let Some(caps) = subprojects_pattern.captures(&stdout) {
163 let value = caps.get(1).map(|m| m.as_str().trim()).unwrap_or("");
164 props.has_subprojects = value != "[]";
165 }
166
167 Ok(props)
168}
169
170#[async_trait]
171impl ProjectFinder for GradleProjectFinder {
172 fn projects(&self) -> Vec<&Project> {
173 self.projects.values().collect::<Vec<_>>()
174 }
175
176 fn projects_mut(&mut self) -> Vec<&mut Project> {
177 self.projects.values_mut().collect::<Vec<_>>()
178 }
179
180 fn project_files(&self) -> &[&str] {
181 &self.project_files
182 }
183
184 async fn visit(&mut self, path: &Path, relative_path: &Path) -> Result<()> {
185 if path.is_file()
186 && self.project_files().contains(
187 &path
188 .file_name()
189 .context(format!("File name not found - {}", path.display()))?
190 .to_str()
191 .context(format!("File name not found - {}", path.display()))?,
192 )
193 {
194 if self.projects.contains_key(path) {
195 return Ok(());
196 }
197
198 let project_dir = path
199 .parent()
200 .context(format!("Parent not found - {}", path.display()))?;
201
202 let props = get_gradle_properties(project_dir).await?;
204
205 let name = props.name.or_else(|| {
207 project_dir
208 .file_name()
209 .and_then(|n| n.to_str())
210 .map(std::string::ToString::to_string)
211 });
212
213 let version = props.version;
214
215 let is_workspace = props.has_subprojects;
219
220 let (path, project) = if is_workspace {
221 (
222 path.to_path_buf(),
223 Project::Workspace(Box::new(GradleWorkspace::new(
224 name,
225 version,
226 path.to_path_buf(),
227 relative_path.to_path_buf(),
228 ))),
229 )
230 } else {
231 (
232 path.to_path_buf(),
233 Project::Package(Box::new(GradlePackage::new(
234 name,
235 version,
236 path.to_path_buf(),
237 relative_path.to_path_buf(),
238 ))),
239 )
240 };
241
242 self.projects.insert(path, project);
243 }
244 Ok(())
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use changepacks_core::Project;
252 use std::fs;
253 use tempfile::TempDir;
254
255 #[test]
256 fn test_gradle_project_finder_new() {
257 let finder = GradleProjectFinder::new();
258 assert_eq!(
259 finder.project_files(),
260 &["build.gradle.kts", "build.gradle"]
261 );
262 assert_eq!(finder.projects().len(), 0);
263 }
264
265 #[test]
266 fn test_gradle_project_finder_default() {
267 let finder = GradleProjectFinder::default();
268 assert_eq!(
269 finder.project_files(),
270 &["build.gradle.kts", "build.gradle"]
271 );
272 assert_eq!(finder.projects().len(), 0);
273 }
274
275 fn create_mock_gradlew(dir: &Path, name: &str, version: &str) {
277 if cfg!(windows) {
278 fs::write(
279 dir.join("gradlew.bat"),
280 format!(
281 "@echo off\necho name: {name}\necho version: {version}\necho subprojects: []\n"
282 ),
283 )
284 .unwrap();
285 } else {
286 let gradlew_path = dir.join("gradlew");
287 fs::write(&gradlew_path, format!("#!/bin/sh\necho 'name: {name}'\necho 'version: {version}'\necho 'subprojects: []'\n")).unwrap();
288 #[cfg(unix)]
289 {
290 use std::os::unix::fs::PermissionsExt;
291 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
292 }
293 }
294 }
295
296 #[tokio::test]
297 async fn test_gradle_project_finder_visit_kts_package() {
298 let temp_dir = TempDir::new().unwrap();
299 let project_dir = temp_dir.path().join("myproject");
300 fs::create_dir_all(&project_dir).unwrap();
301
302 let build_gradle = project_dir.join("build.gradle.kts");
303 fs::write(
304 &build_gradle,
305 r#"
306plugins {
307 id("java")
308}
309
310group = "com.example"
311version = "1.0.0"
312"#,
313 )
314 .unwrap();
315
316 create_mock_gradlew(&project_dir, "myproject", "1.0.0");
317
318 let mut finder = GradleProjectFinder::new();
319 finder
320 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
321 .await
322 .unwrap();
323
324 let projects = finder.projects();
325 assert_eq!(projects.len(), 1);
326 match projects[0] {
327 Project::Package(pkg) => {
328 assert_eq!(pkg.name(), Some("myproject"));
329 assert_eq!(pkg.version(), Some("1.0.0"));
330 }
331 _ => panic!("Expected Package"),
332 }
333
334 temp_dir.close().unwrap();
335 }
336
337 #[tokio::test]
338 async fn test_gradle_project_finder_visit_groovy_package() {
339 let temp_dir = TempDir::new().unwrap();
340 let project_dir = temp_dir.path().join("groovyproject");
341 fs::create_dir_all(&project_dir).unwrap();
342
343 let build_gradle = project_dir.join("build.gradle");
344 fs::write(
345 &build_gradle,
346 r#"
347plugins {
348 id 'java'
349}
350
351group = 'com.example'
352version = '2.0.0'
353"#,
354 )
355 .unwrap();
356
357 create_mock_gradlew(&project_dir, "groovyproject", "2.0.0");
358
359 let mut finder = GradleProjectFinder::new();
360 finder
361 .visit(&build_gradle, &PathBuf::from("groovyproject/build.gradle"))
362 .await
363 .unwrap();
364
365 let projects = finder.projects();
366 assert_eq!(projects.len(), 1);
367 match projects[0] {
368 Project::Package(pkg) => {
369 assert_eq!(pkg.name(), Some("groovyproject"));
370 assert_eq!(pkg.version(), Some("2.0.0"));
371 }
372 _ => panic!("Expected Package"),
373 }
374
375 temp_dir.close().unwrap();
376 }
377
378 #[tokio::test]
379 async fn test_gradle_project_finder_visit_workspace() {
380 let temp_dir = TempDir::new().unwrap();
381 let project_dir = temp_dir.path().join("multiproject");
382 fs::create_dir_all(&project_dir).unwrap();
383
384 let build_gradle = project_dir.join("build.gradle.kts");
385 fs::write(
386 &build_gradle,
387 r#"
388plugins {
389 id("java")
390}
391
392group = "com.example"
393version = "1.0.0"
394"#,
395 )
396 .unwrap();
397
398 if cfg!(windows) {
400 fs::write(project_dir.join("gradlew.bat"), "@echo off\necho name: multiproject\necho version: 1.0.0\necho subprojects: [project ':subproject1', project ':subproject2']\n").unwrap();
401 } else {
402 let gradlew_path = project_dir.join("gradlew");
403 fs::write(&gradlew_path, "#!/bin/sh\necho 'name: multiproject'\necho 'version: 1.0.0'\necho \"subprojects: [project ':subproject1', project ':subproject2']\"\n").unwrap();
404 #[cfg(unix)]
405 {
406 use std::os::unix::fs::PermissionsExt;
407 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
408 }
409 }
410
411 let mut finder = GradleProjectFinder::new();
412 finder
413 .visit(
414 &build_gradle,
415 &PathBuf::from("multiproject/build.gradle.kts"),
416 )
417 .await
418 .unwrap();
419
420 let projects = finder.projects();
421 assert_eq!(projects.len(), 1);
422 match projects[0] {
423 Project::Workspace(ws) => {
424 assert_eq!(ws.name(), Some("multiproject"));
425 assert_eq!(ws.version(), Some("1.0.0"));
426 }
427 _ => panic!("Expected Workspace"),
428 }
429
430 temp_dir.close().unwrap();
431 }
432
433 #[tokio::test]
434 async fn test_gradle_project_finder_settings_file_does_not_make_workspace() {
435 let temp_dir = TempDir::new().unwrap();
438 let project_dir = temp_dir.path().join("myproject");
439 fs::create_dir_all(&project_dir).unwrap();
440
441 let build_gradle = project_dir.join("build.gradle.kts");
442 fs::write(&build_gradle, "version = \"1.0.0\"\n").unwrap();
443
444 fs::write(
446 project_dir.join("settings.gradle.kts"),
447 "rootProject.name = \"myproject\"\n",
448 )
449 .unwrap();
450
451 create_mock_gradlew(&project_dir, "myproject", "1.0.0");
452
453 let mut finder = GradleProjectFinder::new();
454 finder
455 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
456 .await
457 .unwrap();
458
459 let projects = finder.projects();
460 assert_eq!(projects.len(), 1);
461 match projects[0] {
462 Project::Package(_) => {} _ => panic!("Expected Package, not Workspace"),
464 }
465
466 temp_dir.close().unwrap();
467 }
468
469 #[tokio::test]
470 async fn test_gradle_project_finder_empty_subprojects_is_package() {
471 let temp_dir = TempDir::new().unwrap();
473 let project_dir = temp_dir.path().join("standalone");
474 fs::create_dir_all(&project_dir).unwrap();
475
476 let build_gradle = project_dir.join("build.gradle.kts");
477 fs::write(&build_gradle, "version = \"1.0.0\"\n").unwrap();
478
479 if cfg!(windows) {
480 fs::write(
481 project_dir.join("gradlew.bat"),
482 "@echo off\necho name: standalone\necho version: 1.0.0\necho subprojects: []\n",
483 )
484 .unwrap();
485 } else {
486 let gradlew_path = project_dir.join("gradlew");
487 fs::write(&gradlew_path, "#!/bin/sh\necho 'name: standalone'\necho 'version: 1.0.0'\necho 'subprojects: []'\n").unwrap();
488 #[cfg(unix)]
489 {
490 use std::os::unix::fs::PermissionsExt;
491 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
492 }
493 }
494
495 let mut finder = GradleProjectFinder::new();
496 finder
497 .visit(&build_gradle, &PathBuf::from("standalone/build.gradle.kts"))
498 .await
499 .unwrap();
500
501 let projects = finder.projects();
502 assert_eq!(projects.len(), 1);
503 match projects[0] {
504 Project::Package(pkg) => {
505 assert_eq!(pkg.name(), Some("standalone"));
506 }
507 _ => panic!("Expected Package, not Workspace"),
508 }
509
510 temp_dir.close().unwrap();
511 }
512
513 #[tokio::test]
514 async fn test_gradle_project_finder_visit_non_gradle_file() {
515 let temp_dir = TempDir::new().unwrap();
516 let other_file = temp_dir.path().join("other.txt");
517 fs::write(&other_file, "some content").unwrap();
518
519 let mut finder = GradleProjectFinder::new();
520 finder
521 .visit(&other_file, &PathBuf::from("other.txt"))
522 .await
523 .unwrap();
524
525 assert_eq!(finder.projects().len(), 0);
526
527 temp_dir.close().unwrap();
528 }
529
530 #[tokio::test]
531 async fn test_gradle_project_finder_visit_duplicate() {
532 let temp_dir = TempDir::new().unwrap();
533 let project_dir = temp_dir.path().join("myproject");
534 fs::create_dir_all(&project_dir).unwrap();
535
536 let build_gradle = project_dir.join("build.gradle.kts");
537 fs::write(&build_gradle, "version = \"1.0.0\"\n").unwrap();
538
539 create_mock_gradlew(&project_dir, "myproject", "1.0.0");
540
541 let mut finder = GradleProjectFinder::new();
542 finder
543 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
544 .await
545 .unwrap();
546
547 assert_eq!(finder.projects().len(), 1);
548
549 finder
551 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
552 .await
553 .unwrap();
554
555 assert_eq!(finder.projects().len(), 1);
556
557 temp_dir.close().unwrap();
558 }
559
560 #[tokio::test]
561 async fn test_gradle_project_finder_projects_mut() {
562 let temp_dir = TempDir::new().unwrap();
563 let project_dir = temp_dir.path().join("myproject");
564 fs::create_dir_all(&project_dir).unwrap();
565
566 let build_gradle = project_dir.join("build.gradle.kts");
567 fs::write(&build_gradle, "version = \"1.0.0\"\n").unwrap();
568
569 create_mock_gradlew(&project_dir, "myproject", "1.0.0");
570
571 let mut finder = GradleProjectFinder::new();
572 finder
573 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
574 .await
575 .unwrap();
576
577 let mut_projects = finder.projects_mut();
578 assert_eq!(mut_projects.len(), 1);
579
580 temp_dir.close().unwrap();
581 }
582
583 #[test]
584 fn test_find_gradlew_in_same_dir() {
585 let temp_dir = TempDir::new().unwrap();
586
587 if cfg!(windows) {
588 fs::write(temp_dir.path().join("gradlew.bat"), "@echo off").unwrap();
589 } else {
590 fs::write(temp_dir.path().join("gradlew"), "#!/bin/sh").unwrap();
591 }
592
593 let result = find_gradlew(temp_dir.path());
594 assert!(result.is_some());
595 let (_, gradlew_dir) = result.unwrap();
596 assert_eq!(gradlew_dir, temp_dir.path());
597
598 temp_dir.close().unwrap();
599 }
600
601 #[test]
602 fn test_find_gradlew_in_parent_dir() {
603 let temp_dir = TempDir::new().unwrap();
604 let subproject = temp_dir.path().join("libs").join("core");
605 fs::create_dir_all(&subproject).unwrap();
606
607 if cfg!(windows) {
609 fs::write(temp_dir.path().join("gradlew.bat"), "@echo off").unwrap();
610 } else {
611 fs::write(temp_dir.path().join("gradlew"), "#!/bin/sh").unwrap();
612 }
613
614 let result = find_gradlew(&subproject);
615 assert!(result.is_some());
616 let (_, gradlew_dir) = result.unwrap();
617 assert_eq!(gradlew_dir, temp_dir.path().to_path_buf());
618
619 temp_dir.close().unwrap();
620 }
621
622 #[test]
623 fn test_find_gradlew_not_found() {
624 let temp_dir = TempDir::new().unwrap();
625 let subdir = temp_dir.path().join("no_gradlew_here");
626 fs::create_dir_all(&subdir).unwrap();
627
628 let _ = find_gradlew(&subdir);
633
634 temp_dir.close().unwrap();
635 }
636
637 #[tokio::test]
638 async fn test_get_gradle_properties_no_gradlew() {
639 let temp_dir = TempDir::new().unwrap();
640 let subdir = temp_dir.path().join("isolated");
641 fs::create_dir_all(&subdir).unwrap();
642 let result = get_gradle_properties(&subdir).await;
644 let _ = result;
647 temp_dir.close().unwrap();
648 }
649
650 #[tokio::test]
651 async fn test_get_gradle_properties_with_mock() {
652 let temp_dir = TempDir::new().unwrap();
653
654 if cfg!(windows) {
656 let gradlew_path = temp_dir.path().join("gradlew.bat");
657 fs::write(
658 &gradlew_path,
659 "@echo off\necho name: myproject\necho version: 1.2.3\n",
660 )
661 .unwrap();
662 } else {
663 let gradlew_path = temp_dir.path().join("gradlew");
664 fs::write(
665 &gradlew_path,
666 "#!/bin/sh\necho 'name: myproject'\necho 'version: 1.2.3'\n",
667 )
668 .unwrap();
669 #[cfg(unix)]
671 {
672 use std::os::unix::fs::PermissionsExt;
673 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
674 }
675 }
676
677 let props = get_gradle_properties(temp_dir.path()).await.unwrap();
678 assert_eq!(props.name, Some("myproject".to_string()));
679 assert_eq!(props.version, Some("1.2.3".to_string()));
680 assert!(!props.has_subprojects);
681
682 temp_dir.close().unwrap();
683 }
684
685 #[tokio::test]
686 async fn test_get_gradle_properties_with_subprojects() {
687 let temp_dir = TempDir::new().unwrap();
688
689 if cfg!(windows) {
690 fs::write(temp_dir.path().join("gradlew.bat"), "@echo off\necho name: root\necho version: 1.0.0\necho subprojects: [project ':app', project ':lib']\n").unwrap();
691 } else {
692 let gradlew_path = temp_dir.path().join("gradlew");
693 fs::write(&gradlew_path, "#!/bin/sh\necho 'name: root'\necho 'version: 1.0.0'\necho \"subprojects: [project ':app', project ':lib']\"\n").unwrap();
694 #[cfg(unix)]
695 {
696 use std::os::unix::fs::PermissionsExt;
697 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
698 }
699 }
700
701 let props = get_gradle_properties(temp_dir.path()).await.unwrap();
702 assert_eq!(props.name, Some("root".to_string()));
703 assert!(props.has_subprojects);
704
705 temp_dir.close().unwrap();
706 }
707
708 #[tokio::test]
709 async fn test_get_gradle_properties_empty_subprojects() {
710 let temp_dir = TempDir::new().unwrap();
711
712 if cfg!(windows) {
713 fs::write(
714 temp_dir.path().join("gradlew.bat"),
715 "@echo off\necho name: leaf\necho version: 1.0.0\necho subprojects: []\n",
716 )
717 .unwrap();
718 } else {
719 let gradlew_path = temp_dir.path().join("gradlew");
720 fs::write(
721 &gradlew_path,
722 "#!/bin/sh\necho 'name: leaf'\necho 'version: 1.0.0'\necho 'subprojects: []'\n",
723 )
724 .unwrap();
725 #[cfg(unix)]
726 {
727 use std::os::unix::fs::PermissionsExt;
728 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
729 }
730 }
731
732 let props = get_gradle_properties(temp_dir.path()).await.unwrap();
733 assert_eq!(props.name, Some("leaf".to_string()));
734 assert!(!props.has_subprojects);
735
736 temp_dir.close().unwrap();
737 }
738
739 #[tokio::test]
740 async fn test_get_gradle_properties_from_parent_gradlew() {
741 let temp_dir = TempDir::new().unwrap();
742 let subproject = temp_dir.path().join("sub1");
743 fs::create_dir_all(&subproject).unwrap();
744
745 if cfg!(windows) {
747 let gradlew_path = temp_dir.path().join("gradlew.bat");
748 fs::write(
750 &gradlew_path,
751 "@echo off\necho name: sub1\necho version: 2.0.0\n",
752 )
753 .unwrap();
754 } else {
755 let gradlew_path = temp_dir.path().join("gradlew");
756 fs::write(
757 &gradlew_path,
758 "#!/bin/sh\necho 'name: sub1'\necho 'version: 2.0.0'\n",
759 )
760 .unwrap();
761 #[cfg(unix)]
762 {
763 use std::os::unix::fs::PermissionsExt;
764 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
765 }
766 }
767
768 let props = get_gradle_properties(&subproject).await.unwrap();
769 assert_eq!(props.name, Some("sub1".to_string()));
770 assert_eq!(props.version, Some("2.0.0".to_string()));
771
772 temp_dir.close().unwrap();
773 }
774
775 #[tokio::test]
776 async fn test_get_gradle_properties_nested_subproject() {
777 let temp_dir = TempDir::new().unwrap();
778 let subproject = temp_dir.path().join("libs").join("core");
779 fs::create_dir_all(&subproject).unwrap();
780
781 if cfg!(windows) {
783 let gradlew_path = temp_dir.path().join("gradlew.bat");
784 fs::write(
786 &gradlew_path,
787 "@echo off\necho name: core\necho version: 3.1.0\n",
788 )
789 .unwrap();
790 } else {
791 let gradlew_path = temp_dir.path().join("gradlew");
792 fs::write(
793 &gradlew_path,
794 "#!/bin/sh\necho 'name: core'\necho 'version: 3.1.0'\n",
795 )
796 .unwrap();
797 #[cfg(unix)]
798 {
799 use std::os::unix::fs::PermissionsExt;
800 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
801 }
802 }
803
804 let props = get_gradle_properties(&subproject).await.unwrap();
805 assert_eq!(props.name, Some("core".to_string()));
806 assert_eq!(props.version, Some("3.1.0".to_string()));
807
808 temp_dir.close().unwrap();
809 }
810
811 #[tokio::test]
812 async fn test_get_gradle_properties_unspecified() {
813 let temp_dir = TempDir::new().unwrap();
814
815 if cfg!(windows) {
816 let gradlew_path = temp_dir.path().join("gradlew.bat");
817 fs::write(
818 &gradlew_path,
819 "@echo off\necho name: unspecified\necho version: unspecified\n",
820 )
821 .unwrap();
822 } else {
823 let gradlew_path = temp_dir.path().join("gradlew");
824 fs::write(
825 &gradlew_path,
826 "#!/bin/sh\necho 'name: unspecified'\necho 'version: unspecified'\n",
827 )
828 .unwrap();
829 #[cfg(unix)]
830 {
831 use std::os::unix::fs::PermissionsExt;
832 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
833 }
834 }
835
836 let props = get_gradle_properties(temp_dir.path()).await.unwrap();
837 assert!(props.name.is_none());
838 assert!(props.version.is_none());
839
840 temp_dir.close().unwrap();
841 }
842
843 #[tokio::test]
844 async fn test_get_gradle_properties_gradlew_fails() {
845 let temp_dir = TempDir::new().unwrap();
846
847 if cfg!(windows) {
848 let gradlew_path = temp_dir.path().join("gradlew.bat");
849 fs::write(&gradlew_path, "@echo off\nexit /b 1\n").unwrap();
850 } else {
851 let gradlew_path = temp_dir.path().join("gradlew");
852 fs::write(&gradlew_path, "#!/bin/sh\nexit 1\n").unwrap();
853 #[cfg(unix)]
854 {
855 use std::os::unix::fs::PermissionsExt;
856 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
857 }
858 }
859
860 let props = get_gradle_properties(temp_dir.path()).await.unwrap();
862 assert!(props.name.is_none());
863 assert!(props.version.is_none());
864
865 temp_dir.close().unwrap();
866 }
867
868 #[test]
869 fn test_which_java_returns_some_or_none() {
870 let result = which_java();
873 let _ = result;
876 }
877
878 #[test]
879 fn test_which_java_with_empty_path() {
880 let original = std::env::var_os("PATH");
882 unsafe { std::env::set_var("PATH", "") };
884
885 let result = which_java();
886 assert!(result.is_none());
887
888 if let Some(p) = original {
890 unsafe { std::env::set_var("PATH", p) };
892 }
893 }
894
895 #[tokio::test]
896 async fn test_gradle_project_finder_visit_name_fallback_to_dir() {
897 let temp_dir = TempDir::new().unwrap();
899 let project_dir = temp_dir.path().join("my-fallback-project");
900 fs::create_dir_all(&project_dir).unwrap();
901
902 let build_gradle = project_dir.join("build.gradle.kts");
903 fs::write(&build_gradle, "version = \"1.0.0\"\n").unwrap();
904
905 if cfg!(windows) {
907 fs::write(
908 project_dir.join("gradlew.bat"),
909 "@echo off\necho name: unspecified\necho version: 1.0.0\necho subprojects: []\n",
910 )
911 .unwrap();
912 } else {
913 let gradlew_path = project_dir.join("gradlew");
914 fs::write(&gradlew_path, "#!/bin/sh\necho 'name: unspecified'\necho 'version: 1.0.0'\necho 'subprojects: []'\n").unwrap();
915 #[cfg(unix)]
916 {
917 use std::os::unix::fs::PermissionsExt;
918 fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
919 }
920 }
921
922 let mut finder = GradleProjectFinder::new();
923 finder
924 .visit(
925 &build_gradle,
926 &PathBuf::from("my-fallback-project/build.gradle.kts"),
927 )
928 .await
929 .unwrap();
930
931 let projects = finder.projects();
932 assert_eq!(projects.len(), 1);
933 match projects[0] {
934 Project::Package(pkg) => {
935 assert_eq!(pkg.name(), Some("my-fallback-project"));
937 assert_eq!(pkg.version(), Some("1.0.0"));
938 }
939 _ => panic!("Expected Package"),
940 }
941
942 temp_dir.close().unwrap();
943 }
944}