Skip to main content

changepacks_java/
finder.rs

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/// Project info obtained from gradlew properties
37#[derive(Debug, Default)]
38struct GradleProperties {
39    name: Option<String>,
40    version: Option<String>,
41    has_subprojects: bool,
42}
43
44/// Find gradlew executable by walking up the directory tree.
45///
46/// In multi-module Gradle builds, `gradlew` lives at the root while subprojects
47/// only contain `build.gradle.kts`. This function searches upward from `start_dir`
48/// until it finds `gradlew` (Unix) or `gradlew.bat` (Windows).
49///
50/// Returns `(gradlew_path, gradlew_dir)` or `None` if not found.
51fn find_gradlew(start_dir: &Path) -> Option<(PathBuf, PathBuf)> {
52    let gradlew_name = if cfg!(windows) {
53        "gradlew.bat"
54    } else {
55        "gradlew"
56    };
57
58    let mut current = start_dir.to_path_buf();
59    loop {
60        let gradlew = current.join(gradlew_name);
61        if gradlew.exists() {
62            return Some((gradlew, current));
63        }
64        if !current.pop() {
65            return None;
66        }
67    }
68}
69
70/// Get project properties using gradlew command.
71///
72/// Walks up the directory tree to find `gradlew`, then runs it with the correct
73/// subproject path. For a subproject at `root/libs/core/`, this runs:
74/// `./gradlew :libs:core:properties -q` from the root directory.
75async fn get_gradle_properties(project_dir: &Path) -> Option<GradleProperties> {
76    let (gradlew, gradlew_dir) = find_gradlew(project_dir)?;
77
78    // Build the command args based on whether this is the root or a subproject
79    let args: Vec<String> = if gradlew_dir == project_dir {
80        // Root project: ./gradlew properties -q
81        vec!["properties".to_string(), "-q".to_string()]
82    } else {
83        // Subproject: ./gradlew :sub:path:properties -q
84        let relative = project_dir.strip_prefix(&gradlew_dir).ok()?;
85        let gradle_path = relative
86            .components()
87            .filter_map(|c| c.as_os_str().to_str())
88            .collect::<Vec<_>>()
89            .join(":");
90        vec![format!(":{gradle_path}:properties"), "-q".to_string()]
91    };
92
93    let output = Command::new(&gradlew)
94        .args(&args)
95        .current_dir(&gradlew_dir)
96        .stdout(Stdio::piped())
97        .stderr(Stdio::null())
98        .output()
99        .await
100        .ok()?;
101
102    if !output.status.success() {
103        return None;
104    }
105
106    let stdout = String::from_utf8_lossy(&output.stdout);
107    let mut props = GradleProperties::default();
108
109    // Parse properties output
110    // Format: "propertyName: value"
111    let name_pattern = Regex::new(r"(?m)^name:\s*(.+)$").ok()?;
112    let version_pattern = Regex::new(r"(?m)^version:\s*(.+)$").ok()?;
113    let subprojects_pattern = Regex::new(r"(?m)^subprojects:\s*(.+)$").ok()?;
114
115    if let Some(caps) = name_pattern.captures(&stdout) {
116        let name = caps.get(1).map(|m| m.as_str().trim().to_string());
117        if name.as_deref() != Some("unspecified") {
118            props.name = name;
119        }
120    }
121
122    if let Some(caps) = version_pattern.captures(&stdout) {
123        let version = caps.get(1).map(|m| m.as_str().trim().to_string());
124        if version.as_deref() != Some("unspecified") {
125            props.version = version;
126        }
127    }
128
129    // Detect workspace: subprojects is non-empty (e.g. "[project ':sub1', project ':sub2']")
130    if let Some(caps) = subprojects_pattern.captures(&stdout) {
131        let value = caps.get(1).map(|m| m.as_str().trim()).unwrap_or("");
132        props.has_subprojects = value != "[]";
133    }
134
135    Some(props)
136}
137
138#[async_trait]
139impl ProjectFinder for GradleProjectFinder {
140    fn projects(&self) -> Vec<&Project> {
141        self.projects.values().collect::<Vec<_>>()
142    }
143
144    fn projects_mut(&mut self) -> Vec<&mut Project> {
145        self.projects.values_mut().collect::<Vec<_>>()
146    }
147
148    fn project_files(&self) -> &[&str] {
149        &self.project_files
150    }
151
152    async fn visit(&mut self, path: &Path, relative_path: &Path) -> Result<()> {
153        if path.is_file()
154            && self.project_files().contains(
155                &path
156                    .file_name()
157                    .context(format!("File name not found - {}", path.display()))?
158                    .to_str()
159                    .context(format!("File name not found - {}", path.display()))?,
160            )
161        {
162            if self.projects.contains_key(path) {
163                return Ok(());
164            }
165
166            let project_dir = path
167                .parent()
168                .context(format!("Parent not found - {}", path.display()))?;
169
170            // Get properties from gradlew command
171            let props = get_gradle_properties(project_dir).await.unwrap_or_default();
172
173            // Use directory name as fallback for project name
174            let name = props.name.or_else(|| {
175                project_dir
176                    .file_name()
177                    .and_then(|n| n.to_str())
178                    .map(std::string::ToString::to_string)
179            });
180
181            let version = props.version;
182
183            // Workspace detection: gradlew reports non-empty subprojects list.
184            // Previous approach (checking for settings.gradle.kts existence) caused
185            // false positives in composite builds and subprojects with IDE-generated files.
186            let is_workspace = props.has_subprojects;
187
188            let (path, project) = if is_workspace {
189                (
190                    path.to_path_buf(),
191                    Project::Workspace(Box::new(GradleWorkspace::new(
192                        name,
193                        version,
194                        path.to_path_buf(),
195                        relative_path.to_path_buf(),
196                    ))),
197                )
198            } else {
199                (
200                    path.to_path_buf(),
201                    Project::Package(Box::new(GradlePackage::new(
202                        name,
203                        version,
204                        path.to_path_buf(),
205                        relative_path.to_path_buf(),
206                    ))),
207                )
208            };
209
210            self.projects.insert(path, project);
211        }
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use changepacks_core::Project;
220    use std::fs;
221    use tempfile::TempDir;
222
223    #[test]
224    fn test_gradle_project_finder_new() {
225        let finder = GradleProjectFinder::new();
226        assert_eq!(
227            finder.project_files(),
228            &["build.gradle.kts", "build.gradle"]
229        );
230        assert_eq!(finder.projects().len(), 0);
231    }
232
233    #[test]
234    fn test_gradle_project_finder_default() {
235        let finder = GradleProjectFinder::default();
236        assert_eq!(
237            finder.project_files(),
238            &["build.gradle.kts", "build.gradle"]
239        );
240        assert_eq!(finder.projects().len(), 0);
241    }
242
243    #[tokio::test]
244    async fn test_gradle_project_finder_visit_kts_package() {
245        let temp_dir = TempDir::new().unwrap();
246        let project_dir = temp_dir.path().join("myproject");
247        fs::create_dir_all(&project_dir).unwrap();
248
249        let build_gradle = project_dir.join("build.gradle.kts");
250        fs::write(
251            &build_gradle,
252            r#"
253plugins {
254    id("java")
255}
256
257group = "com.example"
258version = "1.0.0"
259"#,
260        )
261        .unwrap();
262
263        let mut finder = GradleProjectFinder::new();
264        finder
265            .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
266            .await
267            .unwrap();
268
269        let projects = finder.projects();
270        assert_eq!(projects.len(), 1);
271        match projects[0] {
272            Project::Package(pkg) => {
273                // Without gradlew, falls back to directory name
274                assert_eq!(pkg.name(), Some("myproject"));
275                // Version is None without gradlew
276                assert_eq!(pkg.version(), None);
277            }
278            _ => panic!("Expected Package"),
279        }
280
281        temp_dir.close().unwrap();
282    }
283
284    #[tokio::test]
285    async fn test_gradle_project_finder_visit_groovy_package() {
286        let temp_dir = TempDir::new().unwrap();
287        let project_dir = temp_dir.path().join("groovyproject");
288        fs::create_dir_all(&project_dir).unwrap();
289
290        let build_gradle = project_dir.join("build.gradle");
291        fs::write(
292            &build_gradle,
293            r#"
294plugins {
295    id 'java'
296}
297
298group = 'com.example'
299version = '2.0.0'
300"#,
301        )
302        .unwrap();
303
304        let mut finder = GradleProjectFinder::new();
305        finder
306            .visit(&build_gradle, &PathBuf::from("groovyproject/build.gradle"))
307            .await
308            .unwrap();
309
310        let projects = finder.projects();
311        assert_eq!(projects.len(), 1);
312        match projects[0] {
313            Project::Package(pkg) => {
314                // Without gradlew, falls back to directory name
315                assert_eq!(pkg.name(), Some("groovyproject"));
316            }
317            _ => panic!("Expected Package"),
318        }
319
320        temp_dir.close().unwrap();
321    }
322
323    #[tokio::test]
324    async fn test_gradle_project_finder_visit_workspace() {
325        let temp_dir = TempDir::new().unwrap();
326        let project_dir = temp_dir.path().join("multiproject");
327        fs::create_dir_all(&project_dir).unwrap();
328
329        let build_gradle = project_dir.join("build.gradle.kts");
330        fs::write(
331            &build_gradle,
332            r#"
333plugins {
334    id("java")
335}
336
337group = "com.example"
338version = "1.0.0"
339"#,
340        )
341        .unwrap();
342
343        // Mock gradlew that reports subprojects (this is what makes it a workspace)
344        if cfg!(windows) {
345            fs::write(
346                project_dir.join("gradlew.bat"),
347                "@echo off\necho name: multiproject\necho version: 1.0.0\necho subprojects: [project ':subproject1', project ':subproject2']\n",
348            )
349            .unwrap();
350        } else {
351            let gradlew_path = project_dir.join("gradlew");
352            fs::write(
353                &gradlew_path,
354                "#!/bin/sh\necho 'name: multiproject'\necho 'version: 1.0.0'\necho \"subprojects: [project ':subproject1', project ':subproject2']\"\n",
355            )
356            .unwrap();
357            #[cfg(unix)]
358            {
359                use std::os::unix::fs::PermissionsExt;
360                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
361            }
362        }
363
364        let mut finder = GradleProjectFinder::new();
365        finder
366            .visit(
367                &build_gradle,
368                &PathBuf::from("multiproject/build.gradle.kts"),
369            )
370            .await
371            .unwrap();
372
373        let projects = finder.projects();
374        assert_eq!(projects.len(), 1);
375        match projects[0] {
376            Project::Workspace(ws) => {
377                assert_eq!(ws.name(), Some("multiproject"));
378                assert_eq!(ws.version(), Some("1.0.0"));
379            }
380            _ => panic!("Expected Workspace"),
381        }
382
383        temp_dir.close().unwrap();
384    }
385
386    #[tokio::test]
387    async fn test_gradle_project_finder_settings_file_does_not_make_workspace() {
388        // Regression: settings.gradle.kts presence alone must NOT classify as Workspace.
389        // Only gradlew's subprojects output determines workspace status.
390        let temp_dir = TempDir::new().unwrap();
391        let project_dir = temp_dir.path().join("myproject");
392        fs::create_dir_all(&project_dir).unwrap();
393
394        let build_gradle = project_dir.join("build.gradle.kts");
395        fs::write(&build_gradle, "version = \"1.0.0\"\n").unwrap();
396
397        // settings.gradle.kts exists but no gradlew → should be Package, not Workspace
398        fs::write(
399            project_dir.join("settings.gradle.kts"),
400            "rootProject.name = \"myproject\"\n",
401        )
402        .unwrap();
403
404        let mut finder = GradleProjectFinder::new();
405        finder
406            .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
407            .await
408            .unwrap();
409
410        let projects = finder.projects();
411        assert_eq!(projects.len(), 1);
412        match projects[0] {
413            Project::Package(_) => {} // correct: no gradlew → no subprojects info → Package
414            _ => panic!("Expected Package, not Workspace"),
415        }
416
417        temp_dir.close().unwrap();
418    }
419
420    #[tokio::test]
421    async fn test_gradle_project_finder_empty_subprojects_is_package() {
422        // A project with gradlew but subprojects: [] is a Package, not Workspace
423        let temp_dir = TempDir::new().unwrap();
424        let project_dir = temp_dir.path().join("standalone");
425        fs::create_dir_all(&project_dir).unwrap();
426
427        let build_gradle = project_dir.join("build.gradle.kts");
428        fs::write(&build_gradle, "version = \"1.0.0\"\n").unwrap();
429
430        if cfg!(windows) {
431            fs::write(
432                project_dir.join("gradlew.bat"),
433                "@echo off\necho name: standalone\necho version: 1.0.0\necho subprojects: []\n",
434            )
435            .unwrap();
436        } else {
437            let gradlew_path = project_dir.join("gradlew");
438            fs::write(
439                &gradlew_path,
440                "#!/bin/sh\necho 'name: standalone'\necho 'version: 1.0.0'\necho 'subprojects: []'\n",
441            )
442            .unwrap();
443            #[cfg(unix)]
444            {
445                use std::os::unix::fs::PermissionsExt;
446                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
447            }
448        }
449
450        let mut finder = GradleProjectFinder::new();
451        finder
452            .visit(&build_gradle, &PathBuf::from("standalone/build.gradle.kts"))
453            .await
454            .unwrap();
455
456        let projects = finder.projects();
457        assert_eq!(projects.len(), 1);
458        match projects[0] {
459            Project::Package(pkg) => {
460                assert_eq!(pkg.name(), Some("standalone"));
461            }
462            _ => panic!("Expected Package, not Workspace"),
463        }
464
465        temp_dir.close().unwrap();
466    }
467
468    #[tokio::test]
469    async fn test_gradle_project_finder_visit_non_gradle_file() {
470        let temp_dir = TempDir::new().unwrap();
471        let other_file = temp_dir.path().join("other.txt");
472        fs::write(&other_file, "some content").unwrap();
473
474        let mut finder = GradleProjectFinder::new();
475        finder
476            .visit(&other_file, &PathBuf::from("other.txt"))
477            .await
478            .unwrap();
479
480        assert_eq!(finder.projects().len(), 0);
481
482        temp_dir.close().unwrap();
483    }
484
485    #[tokio::test]
486    async fn test_gradle_project_finder_visit_duplicate() {
487        let temp_dir = TempDir::new().unwrap();
488        let project_dir = temp_dir.path().join("myproject");
489        fs::create_dir_all(&project_dir).unwrap();
490
491        let build_gradle = project_dir.join("build.gradle.kts");
492        fs::write(
493            &build_gradle,
494            r#"
495group = "com.example"
496version = "1.0.0"
497"#,
498        )
499        .unwrap();
500
501        let mut finder = GradleProjectFinder::new();
502        finder
503            .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
504            .await
505            .unwrap();
506
507        assert_eq!(finder.projects().len(), 1);
508
509        // Visit again - should not add duplicate
510        finder
511            .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
512            .await
513            .unwrap();
514
515        assert_eq!(finder.projects().len(), 1);
516
517        temp_dir.close().unwrap();
518    }
519
520    #[tokio::test]
521    async fn test_gradle_project_finder_projects_mut() {
522        let temp_dir = TempDir::new().unwrap();
523        let project_dir = temp_dir.path().join("myproject");
524        fs::create_dir_all(&project_dir).unwrap();
525
526        let build_gradle = project_dir.join("build.gradle.kts");
527        fs::write(
528            &build_gradle,
529            r#"
530group = "com.example"
531version = "1.0.0"
532"#,
533        )
534        .unwrap();
535
536        let mut finder = GradleProjectFinder::new();
537        finder
538            .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
539            .await
540            .unwrap();
541
542        let mut_projects = finder.projects_mut();
543        assert_eq!(mut_projects.len(), 1);
544
545        temp_dir.close().unwrap();
546    }
547
548    #[test]
549    fn test_find_gradlew_in_same_dir() {
550        let temp_dir = TempDir::new().unwrap();
551
552        if cfg!(windows) {
553            fs::write(temp_dir.path().join("gradlew.bat"), "@echo off").unwrap();
554        } else {
555            fs::write(temp_dir.path().join("gradlew"), "#!/bin/sh").unwrap();
556        }
557
558        let result = find_gradlew(temp_dir.path());
559        assert!(result.is_some());
560        let (_, gradlew_dir) = result.unwrap();
561        assert_eq!(gradlew_dir, temp_dir.path());
562
563        temp_dir.close().unwrap();
564    }
565
566    #[test]
567    fn test_find_gradlew_in_parent_dir() {
568        let temp_dir = TempDir::new().unwrap();
569        let subproject = temp_dir.path().join("libs").join("core");
570        fs::create_dir_all(&subproject).unwrap();
571
572        // gradlew at root, not in subproject
573        if cfg!(windows) {
574            fs::write(temp_dir.path().join("gradlew.bat"), "@echo off").unwrap();
575        } else {
576            fs::write(temp_dir.path().join("gradlew"), "#!/bin/sh").unwrap();
577        }
578
579        let result = find_gradlew(&subproject);
580        assert!(result.is_some());
581        let (_, gradlew_dir) = result.unwrap();
582        assert_eq!(gradlew_dir, temp_dir.path().to_path_buf());
583
584        temp_dir.close().unwrap();
585    }
586
587    #[test]
588    fn test_find_gradlew_not_found() {
589        let temp_dir = TempDir::new().unwrap();
590        let subdir = temp_dir.path().join("no_gradlew_here");
591        fs::create_dir_all(&subdir).unwrap();
592
593        // Don't create gradlew anywhere — but find_gradlew walks to filesystem
594        // root, so this test just verifies it doesn't panic. In practice it
595        // returns None only when no gradlew exists anywhere up the tree.
596        // For a reliable "not found" test, we rely on the no-gradlew properties test below.
597        let _ = find_gradlew(&subdir);
598
599        temp_dir.close().unwrap();
600    }
601
602    #[tokio::test]
603    async fn test_get_gradle_properties_no_gradlew() {
604        let temp_dir = TempDir::new().unwrap();
605        // get_gradle_properties will walk up and may find a system gradlew,
606        // but for a temp dir with no gradlew anywhere close, it returns None
607        // or Some with unrelated properties. The key contract: no crash.
608        let _result = get_gradle_properties(temp_dir.path()).await;
609        temp_dir.close().unwrap();
610    }
611
612    #[tokio::test]
613    async fn test_get_gradle_properties_with_mock() {
614        let temp_dir = TempDir::new().unwrap();
615
616        // Create mock gradlew that outputs properties
617        if cfg!(windows) {
618            let gradlew_path = temp_dir.path().join("gradlew.bat");
619            fs::write(
620                &gradlew_path,
621                "@echo off\necho name: myproject\necho version: 1.2.3\n",
622            )
623            .unwrap();
624        } else {
625            let gradlew_path = temp_dir.path().join("gradlew");
626            fs::write(
627                &gradlew_path,
628                "#!/bin/sh\necho 'name: myproject'\necho 'version: 1.2.3'\n",
629            )
630            .unwrap();
631            // Make executable on Unix
632            #[cfg(unix)]
633            {
634                use std::os::unix::fs::PermissionsExt;
635                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
636            }
637        }
638
639        let result = get_gradle_properties(temp_dir.path()).await;
640        assert!(result.is_some());
641        let props = result.unwrap();
642        assert_eq!(props.name, Some("myproject".to_string()));
643        assert_eq!(props.version, Some("1.2.3".to_string()));
644        assert!(!props.has_subprojects);
645
646        temp_dir.close().unwrap();
647    }
648
649    #[tokio::test]
650    async fn test_get_gradle_properties_with_subprojects() {
651        let temp_dir = TempDir::new().unwrap();
652
653        if cfg!(windows) {
654            fs::write(
655                temp_dir.path().join("gradlew.bat"),
656                "@echo off\necho name: root\necho version: 1.0.0\necho subprojects: [project ':app', project ':lib']\n",
657            )
658            .unwrap();
659        } else {
660            let gradlew_path = temp_dir.path().join("gradlew");
661            fs::write(
662                &gradlew_path,
663                "#!/bin/sh\necho 'name: root'\necho 'version: 1.0.0'\necho \"subprojects: [project ':app', project ':lib']\"\n",
664            )
665            .unwrap();
666            #[cfg(unix)]
667            {
668                use std::os::unix::fs::PermissionsExt;
669                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
670            }
671        }
672
673        let result = get_gradle_properties(temp_dir.path()).await;
674        assert!(result.is_some());
675        let props = result.unwrap();
676        assert_eq!(props.name, Some("root".to_string()));
677        assert!(props.has_subprojects);
678
679        temp_dir.close().unwrap();
680    }
681
682    #[tokio::test]
683    async fn test_get_gradle_properties_empty_subprojects() {
684        let temp_dir = TempDir::new().unwrap();
685
686        if cfg!(windows) {
687            fs::write(
688                temp_dir.path().join("gradlew.bat"),
689                "@echo off\necho name: leaf\necho version: 1.0.0\necho subprojects: []\n",
690            )
691            .unwrap();
692        } else {
693            let gradlew_path = temp_dir.path().join("gradlew");
694            fs::write(
695                &gradlew_path,
696                "#!/bin/sh\necho 'name: leaf'\necho 'version: 1.0.0'\necho 'subprojects: []'\n",
697            )
698            .unwrap();
699            #[cfg(unix)]
700            {
701                use std::os::unix::fs::PermissionsExt;
702                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
703            }
704        }
705
706        let result = get_gradle_properties(temp_dir.path()).await;
707        assert!(result.is_some());
708        let props = result.unwrap();
709        assert_eq!(props.name, Some("leaf".to_string()));
710        assert!(!props.has_subprojects);
711
712        temp_dir.close().unwrap();
713    }
714
715    #[tokio::test]
716    async fn test_get_gradle_properties_from_parent_gradlew() {
717        let temp_dir = TempDir::new().unwrap();
718        let subproject = temp_dir.path().join("sub1");
719        fs::create_dir_all(&subproject).unwrap();
720
721        // Place gradlew at root, query from subproject dir
722        if cfg!(windows) {
723            let gradlew_path = temp_dir.path().join("gradlew.bat");
724            // Mock: ignore the :sub1:properties arg, just output properties
725            fs::write(
726                &gradlew_path,
727                "@echo off\necho name: sub1\necho version: 2.0.0\n",
728            )
729            .unwrap();
730        } else {
731            let gradlew_path = temp_dir.path().join("gradlew");
732            fs::write(
733                &gradlew_path,
734                "#!/bin/sh\necho 'name: sub1'\necho 'version: 2.0.0'\n",
735            )
736            .unwrap();
737            #[cfg(unix)]
738            {
739                use std::os::unix::fs::PermissionsExt;
740                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
741            }
742        }
743
744        let result = get_gradle_properties(&subproject).await;
745        assert!(result.is_some());
746        let props = result.unwrap();
747        assert_eq!(props.name, Some("sub1".to_string()));
748        assert_eq!(props.version, Some("2.0.0".to_string()));
749
750        temp_dir.close().unwrap();
751    }
752
753    #[tokio::test]
754    async fn test_get_gradle_properties_nested_subproject() {
755        let temp_dir = TempDir::new().unwrap();
756        let subproject = temp_dir.path().join("libs").join("core");
757        fs::create_dir_all(&subproject).unwrap();
758
759        // Place gradlew at root, query from libs/core/
760        if cfg!(windows) {
761            let gradlew_path = temp_dir.path().join("gradlew.bat");
762            // The mock script receives ":libs:core:properties" "-q" as args
763            fs::write(
764                &gradlew_path,
765                "@echo off\necho name: core\necho version: 3.1.0\n",
766            )
767            .unwrap();
768        } else {
769            let gradlew_path = temp_dir.path().join("gradlew");
770            fs::write(
771                &gradlew_path,
772                "#!/bin/sh\necho 'name: core'\necho 'version: 3.1.0'\n",
773            )
774            .unwrap();
775            #[cfg(unix)]
776            {
777                use std::os::unix::fs::PermissionsExt;
778                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
779            }
780        }
781
782        let result = get_gradle_properties(&subproject).await;
783        assert!(result.is_some());
784        let props = result.unwrap();
785        assert_eq!(props.name, Some("core".to_string()));
786        assert_eq!(props.version, Some("3.1.0".to_string()));
787
788        temp_dir.close().unwrap();
789    }
790
791    #[tokio::test]
792    async fn test_get_gradle_properties_unspecified() {
793        let temp_dir = TempDir::new().unwrap();
794
795        if cfg!(windows) {
796            let gradlew_path = temp_dir.path().join("gradlew.bat");
797            fs::write(
798                &gradlew_path,
799                "@echo off\necho name: unspecified\necho version: unspecified\n",
800            )
801            .unwrap();
802        } else {
803            let gradlew_path = temp_dir.path().join("gradlew");
804            fs::write(
805                &gradlew_path,
806                "#!/bin/sh\necho 'name: unspecified'\necho 'version: unspecified'\n",
807            )
808            .unwrap();
809            #[cfg(unix)]
810            {
811                use std::os::unix::fs::PermissionsExt;
812                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
813            }
814        }
815
816        let result = get_gradle_properties(temp_dir.path()).await;
817        assert!(result.is_some());
818        let props = result.unwrap();
819        assert!(props.name.is_none());
820        assert!(props.version.is_none());
821
822        temp_dir.close().unwrap();
823    }
824
825    #[tokio::test]
826    async fn test_get_gradle_properties_gradlew_fails() {
827        let temp_dir = TempDir::new().unwrap();
828
829        if cfg!(windows) {
830            let gradlew_path = temp_dir.path().join("gradlew.bat");
831            fs::write(&gradlew_path, "@echo off\nexit /b 1\n").unwrap();
832        } else {
833            let gradlew_path = temp_dir.path().join("gradlew");
834            fs::write(&gradlew_path, "#!/bin/sh\nexit 1\n").unwrap();
835            #[cfg(unix)]
836            {
837                use std::os::unix::fs::PermissionsExt;
838                fs::set_permissions(&gradlew_path, fs::Permissions::from_mode(0o755)).unwrap();
839            }
840        }
841
842        let result = get_gradle_properties(temp_dir.path()).await;
843        assert!(result.is_none());
844
845        temp_dir.close().unwrap();
846    }
847}