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 eed0e0ee..c3a379e0 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.golang.model.GoWorkspace; import io.github.guacsec.trustifyda.providers.javascript.workspace.JsWorkspaceDiscovery; import io.github.guacsec.trustifyda.providers.rust.model.CargoMetadata; @@ -843,7 +844,7 @@ int resolveBatchConcurrency() { } private static final Set DEFAULT_WORKSPACE_DISCOVERY_IGNORE = - Set.of("**/node_modules/**", "**/.git/**"); + Set.of("**/node_modules/**", "**/.git/**", "**/target/**"); /** Merges default ignore patterns, env var overrides, and caller-provided patterns. */ Set resolveIgnorePatterns(Set callerPatterns) { @@ -865,7 +866,8 @@ Set resolveIgnorePatterns(Set callerPatterns) { /** * Detects the workspace ecosystem and discovers manifest paths. Checks for Cargo workspace first - * (Cargo.toml + Cargo.lock), then falls back to JS workspace discovery. + * (Cargo.toml + Cargo.lock), then Maven multi-module (pom.xml), then falls back to JS workspace + * discovery. */ List discoverWorkspaceManifests(Path workspaceDir, Set ignorePatterns) throws IOException { @@ -884,6 +886,15 @@ List discoverWorkspaceManifests(Path workspaceDir, Set ignorePatte } } + // Maven multi-module: pom.xml + Path pomXml = workspaceDir.resolve("pom.xml"); + if (Files.isRegularFile(pomXml)) { + List mavenManifests = discoverMavenModules(workspaceDir, ignorePatterns); + if (!mavenManifests.isEmpty()) { + return mavenManifests; + } + } + // JS workspace: require package.json + a lock file Path packageJson = workspaceDir.resolve("package.json"); boolean hasJsLock = @@ -976,6 +987,128 @@ private List discoverGoWorkspaceModules(Path workspaceDir, Set ign } } + /** + * Discovers Maven multi-module workspace manifests by invoking {@code mvn help:evaluate} to list + * declared modules, then recursively checking each module for nested aggregators. + * + *

Uses {@link JavaMavenProvider#resolveVerifiedMavenBinary} for Maven binary resolution, + * including wrapper support. Always includes the root {@code pom.xml}. Uses a visited set to + * prevent cycles in the module graph. + * + * @param workspaceDir the root directory containing the aggregator pom.xml + * @param ignorePatterns glob patterns for paths to exclude from results + * @return list of discovered pom.xml paths; always includes the root pom.xml even if Maven is + * unavailable + */ + private List discoverMavenModules(Path workspaceDir, Set ignorePatterns) { + Path rootPom = workspaceDir.resolve("pom.xml"); + String mvnBin = JavaMavenProvider.resolveVerifiedMavenBinary(workspaceDir).orElse(null); + if (mvnBin == null) { + LOG.warning("Maven binary not available; returning root pom.xml only"); + return List.of(rootPom); + } + + var visited = new java.util.HashSet(); + var manifestPaths = new ArrayList(); + manifestPaths.add(rootPom); + + collectMavenModules(workspaceDir, mvnBin, visited, manifestPaths); + + return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns); + } + + /** + * Recursively collects Maven module pom.xml paths by invoking {@code mvn help:evaluate} on each + * directory and descending into sub-modules. + * + * @param dir the directory to evaluate for modules + * @param mvnBin the resolved Maven binary path + * @param visited set of already-visited directories to prevent cycles + * @param manifestPaths accumulator list for discovered pom.xml paths + */ + private void collectMavenModules( + Path dir, String mvnBin, java.util.HashSet visited, List manifestPaths) { + Path resolvedDir = dir.toAbsolutePath().normalize(); + if (!visited.add(resolvedDir)) { + return; + } + + List modules = listMavenModules(resolvedDir, mvnBin); + for (String mod : modules) { + Path moduleDir = resolvedDir.resolve(mod); + Path modulePom = moduleDir.resolve("pom.xml"); + if (Files.isRegularFile(modulePom)) { + manifestPaths.add(modulePom); + collectMavenModules(moduleDir, mvnBin, visited, manifestPaths); + } + } + } + + /** + * Invokes {@code mvn help:evaluate -Dexpression=project.modules} on the given directory and + * parses the output to extract module names. + * + * @param dir the directory containing a pom.xml to evaluate + * @param mvnBin the resolved Maven binary path + * @return list of module names, or empty list on failure + */ + private List listMavenModules(Path dir, String mvnBin) { + try { + Path pomFile = dir.resolve("pom.xml"); + Operations.ProcessExecOutput output = + Operations.runProcessGetFullOutput( + dir, + new String[] { + mvnBin, + "help:evaluate", + "-Dexpression=project.modules", + "-q", + "-DforceStdout", + "-f", + pomFile.toString(), + "--batch-mode" + }, + null); + if (output.getExitCode() != 0) { + LOG.warning( + "mvn help:evaluate failed with exit code " + output.getExitCode() + " in " + dir); + return Collections.emptyList(); + } + String raw = output.getOutput().trim(); + if (raw.isEmpty() || "null".equals(raw)) { + return Collections.emptyList(); + } + return parseMavenModuleList(raw); + } catch (Exception e) { + LOG.warning("Failed to list Maven modules in " + dir + ": " + e.getMessage()); + return Collections.emptyList(); + } + } + + /** + * Parses the output of {@code mvn help:evaluate -Dexpression=project.modules} which returns a + * string like {@code [module-a, module-b]}. + * + * @param raw the raw output string + * @return list of module name strings + */ + static List parseMavenModuleList(String raw) { + if (raw == null || raw.isEmpty()) { + return Collections.emptyList(); + } + // Expected format: [module-a, module-b, ...] + java.util.regex.Matcher matcher = + java.util.regex.Pattern.compile("^\\[(.+)]$").matcher(raw.trim()); + if (!matcher.matches()) { + return Collections.emptyList(); + } + String inner = matcher.group(1); + return java.util.Arrays.stream(inner.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + } + /** * 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/providers/JavaMavenProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java index c21acae3..da01e03b 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java @@ -39,7 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.logging.Level; +import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.xml.stream.XMLInputFactory; @@ -431,27 +431,16 @@ private List buildMvnCommandArgs(String... baseArgs) { } private String selectMvnRuntime(final Path manifestPath) { - boolean preferWrapper = Operations.getWrapperPreference(MVN); - if (preferWrapper && manifestPath != null) { - String wrapperName = Operations.isWindows() ? "mvnw.cmd" : "mvnw"; - String mvnw = traverseForMvnw(wrapperName, manifestPath.toString()); - if (mvnw != null) { - try { - // verify maven wrapper is accessible - Operations.runProcess(manifestPath.getParent(), mvnw, ARG_VERSION); - if (debugLoggingIsNeeded()) { - log.info(String.format("using maven wrapper from : %s", mvnw)); - } - return mvnw; - } catch (Exception e) { - log.log( - Level.WARNING, - "Failed to check for mvnw due to: {0} Fall back to use mvn", - e.getMessage()); + Path dir = (manifestPath != null) ? manifestPath.getParent() : null; + if (dir != null) { + Optional resolved = resolveVerifiedMavenBinary(dir); + if (resolved.isPresent()) { + if (debugLoggingIsNeeded()) { + log.info(String.format("using maven binary: %s", resolved.get())); } + return resolved.get(); } } - // If maven wrapper is not requested or not accessible, fall back to use mvn String mvn = Operations.getExecutable(MVN, ARG_VERSION); if (debugLoggingIsNeeded()) { log.info(String.format("using mvn executable from : %s", mvn)); @@ -459,7 +448,28 @@ private String selectMvnRuntime(final Path manifestPath) { return mvn; } - private String traverseForMvnw(String wrapperName, String startingManifest) { + public static Optional resolveVerifiedMavenBinary(Path workspaceDir) { + if (Operations.getWrapperPreference(MVN)) { + String wrapperName = Operations.isWindows() ? "mvnw.cmd" : "mvnw"; + String mvnw = traverseForMvnw(wrapperName, workspaceDir.resolve("pom.xml").toString()); + if (mvnw != null) { + try { + Operations.runProcess(workspaceDir, mvnw, ARG_VERSION); + return Optional.of(mvnw); + } catch (Exception e) { + // wrapper found but not functional — fall through to system mvn + } + } + } + try { + String mvn = Operations.getExecutable(MVN, ARG_VERSION); + return Optional.of(mvn); + } catch (RuntimeException e) { + return Optional.empty(); + } + } + + private static String traverseForMvnw(String wrapperName, String startingManifest) { return traverseForMvnw(wrapperName, startingManifest, null); } diff --git a/src/test/java/io/github/guacsec/trustifyda/impl/MavenWorkspaceDiscoveryTest.java b/src/test/java/io/github/guacsec/trustifyda/impl/MavenWorkspaceDiscoveryTest.java new file mode 100644 index 00000000..62e1d0cc --- /dev/null +++ b/src/test/java/io/github/guacsec/trustifyda/impl/MavenWorkspaceDiscoveryTest.java @@ -0,0 +1,397 @@ +/* + * 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.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 MavenWorkspaceDiscoveryTest { + + private static final Path MAVEN_FIXTURES = + Path.of("src/test/resources/tst_manifests/workspace/maven"); + + // --- parseMavenModuleList tests (pure function, no mocking needed) --- + + /** Verifies that a standard module list output is parsed correctly. */ + @Test + void parseMavenModuleList_standardOutput() { + // Given a typical mvn help:evaluate output + String raw = "[module-a, module-b]"; + + // When parsing + List result = ExhortApi.parseMavenModuleList(raw); + + // Then both modules are returned + assertThat(result).containsExactly("module-a", "module-b"); + } + + /** Verifies that a single module is parsed correctly. */ + @Test + void parseMavenModuleList_singleModule() { + // Given output with one module + String raw = "[parent]"; + + // When parsing + List result = ExhortApi.parseMavenModuleList(raw); + + // Then the single module is returned + assertThat(result).containsExactly("parent"); + } + + /** Verifies that null input returns an empty list. */ + @Test + void parseMavenModuleList_nullInput() { + assertThat(ExhortApi.parseMavenModuleList(null)).isEmpty(); + } + + /** Verifies that empty string returns an empty list. */ + @Test + void parseMavenModuleList_emptyInput() { + assertThat(ExhortApi.parseMavenModuleList("")).isEmpty(); + } + + /** Verifies that 'null' string (no modules) returns an empty list. */ + @Test + void parseMavenModuleList_nullString() { + // "null" is returned by mvn when there are no modules + assertThat(ExhortApi.parseMavenModuleList("null")).isEmpty(); + } + + /** Verifies that malformed output (no brackets) returns an empty list. */ + @Test + void parseMavenModuleList_malformedOutput() { + assertThat(ExhortApi.parseMavenModuleList("module-a, module-b")).isEmpty(); + } + + /** Verifies that whitespace around module names is trimmed. */ + @Test + void parseMavenModuleList_withWhitespace() { + // Given output with extra whitespace + String raw = "[ module-a , module-b ]"; + + // When parsing + List result = ExhortApi.parseMavenModuleList(raw); + + // Then modules are trimmed + assertThat(result).containsExactly("module-a", "module-b"); + } + + // --- discoverWorkspaceManifests tests (require mocking Operations) --- + + /** Verifies that a multi-module Maven project discovers all module pom.xml files. */ + @Test + void discoverWorkspaceManifests_mavenMultiModule() throws IOException { + // Given a multi-module Maven workspace + Path workspaceDir = MAVEN_FIXTURES.resolve("maven_multi_module").toAbsolutePath().normalize(); + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + // Mock Maven binary resolution + mockOps.when(() -> Operations.getWrapperPreference("mvn")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getExecutable("mvn", "-v")).thenReturn("mvn"); + + // Mock mvn help:evaluate for root pom -> returns [module-a, module-b] + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("[module-a, module-b]", "", 0)); + + // Mock mvn help:evaluate for module-a -> returns null (leaf module) + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir.resolve("module-a")), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("null", "", 0)); + + // Mock mvn help:evaluate for module-b -> returns null (leaf module) + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir.resolve("module-b")), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("null", "", 0)); + + // When discovering workspace manifests + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + // Then root + 2 modules = 3 pom.xml files + assertThat(manifests).hasSize(3); + assertThat(manifests).allMatch(p -> p.getFileName().toString().equals("pom.xml")); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml")); + assertThat(manifests).anyMatch(p -> p.toString().contains("module-a")); + assertThat(manifests).anyMatch(p -> p.toString().contains("module-b")); + } + } + + /** Verifies that nested aggregator modules are discovered recursively. */ + @Test + void discoverWorkspaceManifests_nestedAggregator() throws IOException { + // Given a nested Maven aggregator workspace + Path workspaceDir = + MAVEN_FIXTURES.resolve("maven_nested_aggregator").toAbsolutePath().normalize(); + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("mvn")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getExecutable("mvn", "-v")).thenReturn("mvn"); + + // Root -> [parent] + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("[parent]", "", 0)); + + // parent -> [child] + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir.resolve("parent").toAbsolutePath().normalize()), + any(String[].class), + isNull())) + .thenReturn(new Operations.ProcessExecOutput("[child]", "", 0)); + + // child -> null (leaf) + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq( + workspaceDir + .resolve("parent") + .resolve("child") + .toAbsolutePath() + .normalize()), + any(String[].class), + isNull())) + .thenReturn(new Operations.ProcessExecOutput("null", "", 0)); + + // When discovering workspace manifests + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + // Then root + parent + child = 3 pom.xml files + assertThat(manifests).hasSize(3); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml")); + assertThat(manifests) + .anyMatch(p -> p.toString().contains("parent" + java.io.File.separator + "pom.xml")); + assertThat(manifests) + .anyMatch( + p -> + p.toString() + .contains( + "parent" + + java.io.File.separator + + "child" + + java.io.File.separator + + "pom.xml")); + } + } + + /** Verifies that a project with no modules returns only the root pom.xml. */ + @Test + void discoverWorkspaceManifests_noModules() throws IOException { + // Given a Maven project with no modules + Path workspaceDir = MAVEN_FIXTURES.resolve("maven_no_modules").toAbsolutePath().normalize(); + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("mvn")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getExecutable("mvn", "-v")).thenReturn("mvn"); + + // Root -> null (no modules) + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("null", "", 0)); + + // When discovering workspace manifests + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + // Then only the root pom.xml is returned + assertThat(manifests).hasSize(1); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml")); + } + } + + /** Verifies that missing module directories are skipped gracefully. */ + @Test + void discoverWorkspaceManifests_missingModuleDirectory() throws IOException { + // Given a Maven project where one module directory is missing + Path workspaceDir = MAVEN_FIXTURES.resolve("maven_missing_module").toAbsolutePath().normalize(); + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("mvn")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getExecutable("mvn", "-v")).thenReturn("mvn"); + + // Root -> [module-a, module-missing] + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("[module-a, module-missing]", "", 0)); + + // module-a -> null (leaf) + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir.resolve("module-a").toAbsolutePath().normalize()), + any(String[].class), + isNull())) + .thenReturn(new Operations.ProcessExecOutput("null", "", 0)); + + // When discovering workspace manifests + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + // Then root + module-a = 2 (module-missing is skipped) + assertThat(manifests).hasSize(2); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml")); + assertThat(manifests).anyMatch(p -> p.toString().contains("module-a")); + assertThat(manifests).noneMatch(p -> p.toString().contains("module-missing")); + } + } + + /** Verifies that when mvn command fails, the root pom.xml is still returned. */ + @Test + void discoverWorkspaceManifests_mvnCommandFails() throws IOException { + // Given a Maven workspace where mvn invocation fails + Path workspaceDir = MAVEN_FIXTURES.resolve("maven_multi_module").toAbsolutePath().normalize(); + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("mvn")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getExecutable("mvn", "-v")).thenReturn("mvn"); + + // mvn command fails with non-zero exit code + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("", "error", 1)); + + // When discovering workspace manifests + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of()); + + // Then only the root pom.xml is returned (mvn failure falls through gracefully) + assertThat(manifests).hasSize(1); + assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml")); + } + } + + /** Verifies that ignore patterns filter out matched module paths. */ + @Test + void discoverWorkspaceManifests_ignorePatternFiltering() throws IOException { + // Given a multi-module Maven workspace with an ignore pattern + Path workspaceDir = MAVEN_FIXTURES.resolve("maven_multi_module").toAbsolutePath().normalize(); + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("mvn")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getExecutable("mvn", "-v")).thenReturn("mvn"); + + // Root -> [module-a, module-b] + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("[module-a, module-b]", "", 0)); + + // module-a -> null + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir.resolve("module-a").toAbsolutePath().normalize()), + any(String[].class), + isNull())) + .thenReturn(new Operations.ProcessExecOutput("null", "", 0)); + + // module-b -> null + mockOps + .when( + () -> + Operations.runProcessGetFullOutput( + eq(workspaceDir.resolve("module-b").toAbsolutePath().normalize()), + any(String[].class), + isNull())) + .thenReturn(new Operations.ProcessExecOutput("null", "", 0)); + + // When discovering with ignore pattern for module-b + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + List manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of("**/module-b/**")); + + // Then module-b is filtered out + assertThat(manifests).anyMatch(p -> p.toString().contains("module-a")); + assertThat(manifests).noneMatch(p -> p.toString().contains("module-b")); + } + } + + /** Verifies that the default ignore patterns include target directories. */ + @Test + void defaultIgnorePatterns_includesTarget() throws IOException { + // Given a multi-module project with a pom in a target directory + // The default patterns should include **/target/** + Path workspaceDir = MAVEN_FIXTURES.resolve("maven_multi_module").toAbsolutePath().normalize(); + + try (MockedStatic mockOps = Mockito.mockStatic(Operations.class)) { + mockOps.when(() -> Operations.getWrapperPreference("mvn")).thenReturn(false); + mockOps.when(() -> Operations.isWindows()).thenReturn(false); + mockOps.when(() -> Operations.getExecutable("mvn", "-v")).thenReturn("mvn"); + + // Root -> [module-a] + mockOps + .when(() -> Operations.runProcessGetFullOutput(any(), any(String[].class), isNull())) + .thenReturn(new Operations.ProcessExecOutput("null", "", 0)); + + // When resolving default ignore patterns + ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class)); + Set resolvedPatterns = api.resolveIgnorePatterns(null); + + // Then **/target/** is included + assertThat(resolvedPatterns).contains("**/target/**"); + assertThat(resolvedPatterns).contains("**/node_modules/**"); + assertThat(resolvedPatterns).contains("**/.git/**"); + } + } +} diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_missing_module/module-a/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_missing_module/module-a/pom.xml new file mode 100644 index 00000000..6740fbdd --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_missing_module/module-a/pom.xml @@ -0,0 +1,9 @@ + + 4.0.0 + + com.example + parent + 1.0.0 + + module-a + diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_missing_module/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_missing_module/pom.xml new file mode 100644 index 00000000..1476968b --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_missing_module/pom.xml @@ -0,0 +1,11 @@ + + 4.0.0 + com.example + parent + 1.0.0 + pom + + module-a + module-missing + + diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/module-a/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/module-a/pom.xml new file mode 100644 index 00000000..6740fbdd --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/module-a/pom.xml @@ -0,0 +1,9 @@ + + 4.0.0 + + com.example + parent + 1.0.0 + + module-a + diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/module-b/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/module-b/pom.xml new file mode 100644 index 00000000..03684ec4 --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/module-b/pom.xml @@ -0,0 +1,9 @@ + + 4.0.0 + + com.example + parent + 1.0.0 + + module-b + diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/pom.xml new file mode 100644 index 00000000..0566726e --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_multi_module/pom.xml @@ -0,0 +1,11 @@ + + 4.0.0 + com.example + parent + 1.0.0 + pom + + module-a + module-b + + diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/parent/child/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/parent/child/pom.xml new file mode 100644 index 00000000..b29df9e8 --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/parent/child/pom.xml @@ -0,0 +1,9 @@ + + 4.0.0 + + com.example + parent + 1.0.0 + + child + diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/parent/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/parent/pom.xml new file mode 100644 index 00000000..b95c8a95 --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/parent/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + com.example + parent + 1.0.0 + pom + + child + + diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/pom.xml new file mode 100644 index 00000000..144babcf --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_nested_aggregator/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + com.example + root + 1.0.0 + pom + + parent + + diff --git a/src/test/resources/tst_manifests/workspace/maven/maven_no_modules/pom.xml b/src/test/resources/tst_manifests/workspace/maven/maven_no_modules/pom.xml new file mode 100644 index 00000000..770a783f --- /dev/null +++ b/src/test/resources/tst_manifests/workspace/maven/maven_no_modules/pom.xml @@ -0,0 +1,6 @@ + + 4.0.0 + com.example + single + 1.0.0 +