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