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