diff --git a/src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java b/src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java index 3a7d6c60..0a09e391 100644 --- a/src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java +++ b/src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java @@ -30,6 +30,7 @@ import io.github.guacsec.trustifyda.image.ImageUtils; import io.github.guacsec.trustifyda.license.LicenseCheck; import io.github.guacsec.trustifyda.logging.LoggersFactory; +import io.github.guacsec.trustifyda.providers.JavaMavenProvider; import io.github.guacsec.trustifyda.providers.javascript.workspace.JsWorkspaceDiscovery; import io.github.guacsec.trustifyda.providers.rust.model.CargoMetadata; import io.github.guacsec.trustifyda.tools.Ecosystem; @@ -842,7 +843,7 @@ int resolveBatchConcurrency() { } private static final Set DEFAULT_WORKSPACE_DISCOVERY_IGNORE = - Set.of("**/node_modules/**", "**/.git/**"); + Set.of("**/node_modules/**", "**/.git/**", "**/target/**", "**/build/**", "**/.gradle/**"); /** Merges default ignore patterns, env var overrides, and caller-provided patterns. */ Set resolveIgnorePatterns(Set callerPatterns) { @@ -875,6 +876,14 @@ List discoverWorkspaceManifests(Path workspaceDir, Set ignorePatte return discoverCargoManifests(workspaceDir, ignorePatterns); } + // Gradle multi-project: settings.gradle or settings.gradle.kts + boolean hasGradleSettings = + Files.isRegularFile(workspaceDir.resolve("settings.gradle")) + || Files.isRegularFile(workspaceDir.resolve("settings.gradle.kts")); + if (hasGradleSettings) { + return discoverGradleSubprojects(workspaceDir, ignorePatterns); + } + // JS workspace: require package.json + a lock file Path packageJson = workspaceDir.resolve("package.json"); boolean hasJsLock = @@ -930,6 +939,131 @@ private List discoverCargoManifests(Path workspaceDir, Set ignoreP } } + private static final String GRADLE_INIT_SCRIPT = + "allprojects {\n" + + " task daListProjects {\n" + + " doLast {\n" + + " println \"::DA_PROJECT::${project.path}::${project.projectDir}\"\n" + + " }\n" + + " }\n" + + "}\n"; + + /** + * Resolve the Gradle binary, preferring gradlew wrapper when available and configured. + * + * @param startDir directory from which to start the wrapper search + * @return path to the Gradle binary + */ + private static String resolveGradleBinary(Path startDir) { + if (Operations.getWrapperPreference("gradle")) { + String wrapperName = Operations.isWindows() ? "gradlew.bat" : "gradlew"; + String wrapper = + JavaMavenProvider.traverseForMvnw( + wrapperName, startDir.resolve("build.gradle").toString(), null); + if (wrapper != null) { + return wrapper; + } + } + return Operations.getCustomPathOrElse("gradle"); + } + + /** + * Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build. Uses a custom + * init script to get a structured project listing. + */ + private List discoverGradleSubprojects(Path workspaceDir, Set ignorePatterns) { + Path rootBuildKts = workspaceDir.resolve("build.gradle.kts"); + Path rootBuild = workspaceDir.resolve("build.gradle"); + + List manifestPaths = new ArrayList<>(); + if (Files.isRegularFile(rootBuildKts)) { + manifestPaths.add(rootBuildKts); + } else if (Files.isRegularFile(rootBuild)) { + manifestPaths.add(rootBuild); + } + + String gradleBin = resolveGradleBinary(workspaceDir); + Path initScriptPath = null; + try { + initScriptPath = Files.createTempFile("da-list-projects-", ".gradle"); + Files.writeString(initScriptPath, GRADLE_INIT_SCRIPT); + + Operations.ProcessExecOutput output = + Operations.runProcessGetFullOutput( + workspaceDir, + new String[] { + gradleBin, + "-q", + "--no-daemon", + "--init-script", + initScriptPath.toString(), + "daListProjects" + }, + null); + + if (output.getExitCode() != 0) { + LOG.warning( + "gradle daListProjects failed with exit code " + + output.getExitCode() + + ": " + + output.getError()); + return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns); + } + + for (var proj : parseGradleInitScriptOutput(output.getOutput())) { + if (":".equals(proj.path())) { + continue; + } + Path projDir = Path.of(proj.dir()).toAbsolutePath().normalize(); + Path buildKts = projDir.resolve("build.gradle.kts"); + Path buildGroovy = projDir.resolve("build.gradle"); + if (Files.isRegularFile(buildKts)) { + manifestPaths.add(buildKts); + } else if (Files.isRegularFile(buildGroovy)) { + manifestPaths.add(buildGroovy); + } + } + } catch (Exception e) { + LOG.warning("Failed to discover Gradle subprojects: " + e.getMessage()); + return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns); + } finally { + if (initScriptPath != null) { + try { + Files.deleteIfExists(initScriptPath); + } catch (IOException ignored) { + } + } + } + + return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns); + } + + record GradleProject(String path, String dir) {} + + static List parseGradleInitScriptOutput(String raw) { + if (raw == null || raw.isBlank()) { + return List.of(); + } + String prefix = "::DA_PROJECT::"; + List projects = new ArrayList<>(); + for (String line : raw.lines().toList()) { + if (!line.startsWith(prefix)) { + continue; + } + String remainder = line.substring(prefix.length()); + int lastSep = remainder.lastIndexOf("::"); + if (lastSep < 0) { + continue; + } + String path = remainder.substring(0, lastSep); + String dir = remainder.substring(lastSep + 2); + if (!path.isEmpty() && !dir.isEmpty()) { + projects.add(new GradleProject(path, dir)); + } + } + return projects; + } + /** * Checks whether a package.json has "private": true, meaning it should not be analyzed as a * publishable package. diff --git a/src/main/java/io/github/guacsec/trustifyda/utils/WorkspaceUtils.java b/src/main/java/io/github/guacsec/trustifyda/utils/WorkspaceUtils.java index 7e56588f..b9df555e 100644 --- a/src/main/java/io/github/guacsec/trustifyda/utils/WorkspaceUtils.java +++ b/src/main/java/io/github/guacsec/trustifyda/utils/WorkspaceUtils.java @@ -43,7 +43,17 @@ public static List filterByIgnorePatterns( List matchers = ignorePatterns.stream() - .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .flatMap( + p -> { + var fs = FileSystems.getDefault(); + java.util.stream.Stream.Builder b = + java.util.stream.Stream.builder(); + b.add(fs.getPathMatcher("glob:" + p)); + if (p.startsWith("**/")) { + b.add(fs.getPathMatcher("glob:" + p.substring(3))); + } + return b.build(); + }) .toList(); return manifests.stream() diff --git a/src/test/java/io/github/guacsec/trustifyda/impl/GradleWorkspaceDiscoveryTest.java b/src/test/java/io/github/guacsec/trustifyda/impl/GradleWorkspaceDiscoveryTest.java new file mode 100644 index 00000000..9fe328d5 --- /dev/null +++ b/src/test/java/io/github/guacsec/trustifyda/impl/GradleWorkspaceDiscoveryTest.java @@ -0,0 +1,355 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; + +import io.github.guacsec.trustifyda.tools.Operations; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class GradleWorkspaceDiscoveryTest { + + private static final Path GRADLE_FIXTURES = + Path.of("src/test/resources/tst_manifests/workspace/gradle"); + + // --- parseGradleInitScriptOutput tests (pure function, no mocking needed) --- + + @Test + void parseGradleInitScriptOutput_standardOutput() { + String raw = + "::DA_PROJECT:::::/home/project\n" + + "::DA_PROJECT:::app::/home/project/app\n" + + "::DA_PROJECT:::lib::/home/project/lib\n"; + + List result = ExhortApi.parseGradleInitScriptOutput(raw); + + assertThat(result).hasSize(3); + assertThat(result.get(0).path()).isEqualTo(":"); + assertThat(result.get(0).dir()).isEqualTo("/home/project"); + assertThat(result.get(1).path()).isEqualTo(":app"); + assertThat(result.get(1).dir()).isEqualTo("/home/project/app"); + } + + @Test + void parseGradleInitScriptOutput_nestedProjects() { + String raw = + "::DA_PROJECT:::::/home/project\n" + + "::DA_PROJECT:::libs:core::/home/project/libs/core\n" + + "::DA_PROJECT:::libs:util::/home/project/libs/util\n"; + + List result = ExhortApi.parseGradleInitScriptOutput(raw); + + assertThat(result).hasSize(3); + assertThat(result.get(1).path()).isEqualTo(":libs:core"); + assertThat(result.get(1).dir()).isEqualTo("/home/project/libs/core"); + } + + @Test + void parseGradleInitScriptOutput_nullInput() { + assertThat(ExhortApi.parseGradleInitScriptOutput(null)).isEmpty(); + } + + @Test + void parseGradleInitScriptOutput_emptyInput() { + assertThat(ExhortApi.parseGradleInitScriptOutput("")).isEmpty(); + } + + @Test + void parseGradleInitScriptOutput_ignoresNonPrefixedLines() { + String raw = "some gradle log output\n::DA_PROJECT:::app::/home/project/app\nmore output\n"; + + List result = ExhortApi.parseGradleInitScriptOutput(raw); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().path()).isEqualTo(":app"); + } + + // --- discoverWorkspaceManifests tests (require mocking Operations) --- + + @Test + void discoverWorkspaceManifests_gradleMultiProject() throws IOException { + Path workspaceDir = + GRADLE_FIXTURES.resolve("gradle_multi_project").toAbsolutePath().normalize(); + + String initScriptOutput = + "::DA_PROJECT:::::" + + workspaceDir + + "\n" + + "::DA_PROJECT:::app::" + + workspaceDir.resolve("app") + + "\n" + + "::DA_PROJECT:::lib::" + + workspaceDir.resolve("lib") + + "\n"; + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("gradle")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput(initScriptOutput, "", 0)); + + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + assertThat(manifests).hasSize(3); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("build.gradle")); + assertThat(manifests) + .anyMatch(p -> p.toString().contains("app" + File.separator + "build.gradle")); + assertThat(manifests) + .anyMatch(p -> p.toString().contains("lib" + File.separator + "build.gradle")); + } + } + + @Test + void discoverWorkspaceManifests_nestedSubprojects() throws IOException { + Path workspaceDir = + GRADLE_FIXTURES.resolve("gradle_nested_subprojects").toAbsolutePath().normalize(); + + String initScriptOutput = + "::DA_PROJECT:::::" + + workspaceDir + + "\n" + + "::DA_PROJECT:::libs:core::" + + workspaceDir.resolve("libs/core") + + "\n" + + "::DA_PROJECT:::libs:util::" + + workspaceDir.resolve("libs/util") + + "\n"; + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("gradle")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput(initScriptOutput, "", 0)); + + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + assertThat(manifests).hasSize(3); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("build.gradle")); + assertThat(manifests) + .anyMatch( + p -> + p.toString() + .contains( + "libs" + File.separator + "core" + File.separator + "build.gradle")); + assertThat(manifests) + .anyMatch( + p -> + p.toString() + .contains( + "libs" + File.separator + "util" + File.separator + "build.gradle")); + } + } + + @Test + void discoverWorkspaceManifests_mixedGroovyAndKotlin() throws IOException { + Path workspaceDir = + GRADLE_FIXTURES.resolve("gradle_mixed_variants").toAbsolutePath().normalize(); + + String initScriptOutput = + "::DA_PROJECT:::::" + + workspaceDir + + "\n" + + "::DA_PROJECT:::app::" + + workspaceDir.resolve("app") + + "\n" + + "::DA_PROJECT:::lib::" + + workspaceDir.resolve("lib") + + "\n"; + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("gradle")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput(initScriptOutput, "", 0)); + + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + assertThat(manifests).hasSize(3); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("build.gradle.kts")); + assertThat(manifests) + .anyMatch(p -> p.toString().endsWith("app" + File.separator + "build.gradle")); + assertThat(manifests) + .anyMatch(p -> p.toString().endsWith("lib" + File.separator + "build.gradle.kts")); + } + } + + @Test + void discoverWorkspaceManifests_noSubprojects() throws IOException { + Path workspaceDir = + GRADLE_FIXTURES.resolve("gradle_no_subprojects").toAbsolutePath().normalize(); + + String initScriptOutput = "::DA_PROJECT:::::" + workspaceDir + "\n"; + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("gradle")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput(initScriptOutput, "", 0)); + + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + assertThat(manifests).hasSize(1); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("build.gradle")); + } + } + + @Test + void discoverWorkspaceManifests_gradleCommandFails() throws IOException { + Path workspaceDir = + GRADLE_FIXTURES.resolve("gradle_multi_project").toAbsolutePath().normalize(); + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("gradle")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("", "error", 1)); + + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + assertThat(manifests).hasSize(1); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("build.gradle")); + } + } + + @Test + void discoverWorkspaceManifests_missingSubprojectDirectory() throws IOException { + Path workspaceDir = + GRADLE_FIXTURES.resolve("gradle_missing_subproject").toAbsolutePath().normalize(); + + String initScriptOutput = + "::DA_PROJECT:::::" + + workspaceDir + + "\n" + + "::DA_PROJECT:::app::" + + workspaceDir.resolve("app") + + "\n" + + "::DA_PROJECT:::lib-missing::" + + workspaceDir.resolve("lib-missing") + + "\n"; + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("gradle")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput(initScriptOutput, "", 0)); + + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + assertThat(manifests).hasSize(2); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("build.gradle")); + assertThat(manifests).anyMatch(p -> p.toString().contains("app")); + assertThat(manifests).noneMatch(p -> p.toString().contains("lib-missing")); + } + } + + @Test + void discoverWorkspaceManifests_ignorePatternFiltering() throws IOException { + Path workspaceDir = + GRADLE_FIXTURES.resolve("gradle_multi_project").toAbsolutePath().normalize(); + + String initScriptOutput = + "::DA_PROJECT:::::" + + workspaceDir + + "\n" + + "::DA_PROJECT:::app::" + + workspaceDir.resolve("app") + + "\n" + + "::DA_PROJECT:::lib::" + + workspaceDir.resolve("lib") + + "\n"; + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("gradle")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput(initScriptOutput, "", 0)); + + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of("**/lib/**")); + + assertThat(manifests).anyMatch(p -> p.toString().contains("app")); + assertThat(manifests).noneMatch(p -> p.toString().contains("lib")); + } + } + + @Test + void defaultIgnorePatterns_includesBuildAndGradle() { + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + Set resolvedPatterns = api.resolveIgnorePatterns(null); + + assertThat(resolvedPatterns).contains("**/build/**"); + assertThat(resolvedPatterns).contains("**/.gradle/**"); + } +} diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_missing_subproject/app/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_missing_subproject/app/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_missing_subproject/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_missing_subproject/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_missing_subproject/settings.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_missing_subproject/settings.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_mixed_variants/app/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_mixed_variants/app/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_mixed_variants/build.gradle.kts b/src/test/resources/tst_manifests/workspace/gradle/gradle_mixed_variants/build.gradle.kts new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_mixed_variants/lib/build.gradle.kts b/src/test/resources/tst_manifests/workspace/gradle/gradle_mixed_variants/lib/build.gradle.kts new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_mixed_variants/settings.gradle.kts b/src/test/resources/tst_manifests/workspace/gradle/gradle_mixed_variants/settings.gradle.kts new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_multi_project/app/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_multi_project/app/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_multi_project/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_multi_project/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_multi_project/lib/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_multi_project/lib/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_multi_project/settings.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_multi_project/settings.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_nested_subprojects/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_nested_subprojects/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_nested_subprojects/libs/core/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_nested_subprojects/libs/core/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_nested_subprojects/libs/util/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_nested_subprojects/libs/util/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_nested_subprojects/settings.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_nested_subprojects/settings.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_no_subprojects/build.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_no_subprojects/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/tst_manifests/workspace/gradle/gradle_no_subprojects/settings.gradle b/src/test/resources/tst_manifests/workspace/gradle/gradle_no_subprojects/settings.gradle new file mode 100644 index 00000000..e69de29b