From 1d87abbad8b1182dda8be96e1ff93c25af1e258b Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Thu, 12 Feb 2026 19:58:20 +0100 Subject: [PATCH 01/21] Refactor artifact mapping and introduce DependencyGraphMojo implementation --- pom.xml | 4 +- .../bsels/semantic/version/BaseMojo.java | 6 +- .../version/CreateVersionMarkdownMojo.java | 2 +- .../semantic/version/DependencyGraphMojo.java | 57 +++++++++++++++++++ .../bsels/semantic/version/UpdatePomMojo.java | 2 +- .../bsels/semantic/version/VerifyMojo.java | 4 +- .../bsels/semantic/version/utils/Utils.java | 13 +++++ 7 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java diff --git a/pom.xml b/pom.xml index 654c600..e85133e 100644 --- a/pom.xml +++ b/pom.xml @@ -4,11 +4,11 @@ io.github.bsels semantic-version-maven-plugin - 1.2.0 + 1.2.0-SNAPSHOT maven-plugin ${project.groupId}:${project.artifactId} - A Maven plugin that automates semantic versioning with markdown‑based changelog management. + A Maven plugin that automates semantic versioning with Markdown‑based changelog management. Provides two standalone goals: create generates interactive version spec files; update applies those specs, bumps project versions, and merges CHANGELOG entries. diff --git a/src/main/java/io/github/bsels/semantic/version/BaseMojo.java b/src/main/java/io/github/bsels/semantic/version/BaseMojo.java index ce70465..94ac23d 100644 --- a/src/main/java/io/github/bsels/semantic/version/BaseMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/BaseMojo.java @@ -62,7 +62,7 @@ /// /// Any issues encountered during plugin execution may result in a [MojoExecutionException] /// or a [MojoFailureException] being thrown. -public abstract sealed class BaseMojo extends AbstractMojo permits CreateVersionMarkdownMojo, UpdatePomMojo, VerifyMojo { +public abstract sealed class BaseMojo extends AbstractMojo permits CreateVersionMarkdownMojo, DependencyGraphMojo, UpdatePomMojo, VerifyMojo { /// A constant string representing the filename of the changelog file, "CHANGELOG.md". /// @@ -305,7 +305,7 @@ protected MarkdownMapping getMarkdownMapping(List versionMarkdo protected void validateMarkdowns(MarkdownMapping markdownMapping) throws MojoFailureException { Set artifactsInMarkdown = markdownMapping.versionBumpMap().keySet(); Stream projectsInScope = getProjectsInScope(); - Set artifacts = projectsInScope.map(project -> new MavenArtifact(project.getGroupId(), project.getArtifactId())) + Set artifacts = projectsInScope.map(Utils::mavenProjectToArtifact) .collect(Utils.asImmutableSet()); if (!artifacts.containsAll(artifactsInMarkdown)) { @@ -418,7 +418,7 @@ protected Map readAllPoms(List documents = new HashMap<>(); for (MavenProject project : projects) { - MavenArtifact mavenArtifact = new MavenArtifact(project.getGroupId(), project.getArtifactId()); + MavenArtifact mavenArtifact = Utils.mavenProjectToArtifact(project); Path pomFile = project.getFile().toPath(); MavenProjectAndDocument projectAndDocument = new MavenProjectAndDocument( mavenArtifact, diff --git a/src/main/java/io/github/bsels/semantic/version/CreateVersionMarkdownMojo.java b/src/main/java/io/github/bsels/semantic/version/CreateVersionMarkdownMojo.java index 14b8575..6b7b5a2 100644 --- a/src/main/java/io/github/bsels/semantic/version/CreateVersionMarkdownMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/CreateVersionMarkdownMojo.java @@ -107,7 +107,7 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep Log log = getLog(); List projects = getProjectsInScope() - .map(mavenProject -> new MavenArtifact(mavenProject.getGroupId(), mavenProject.getArtifactId())) + .map(Utils::mavenProjectToArtifact) .toList(); if (projects.isEmpty()) { log.warn("No projects found in scope"); diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java new file mode 100644 index 0000000..26a2897 --- /dev/null +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -0,0 +1,57 @@ +package io.github.bsels.semantic.version; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.bsels.semantic.version.models.MavenArtifact; +import io.github.bsels.semantic.version.utils.Utils; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Execute; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Mojo(name = "graph", aggregator = true, requiresDependencyResolution = ResolutionScope.NONE) +@Execute(phase = LifecyclePhase.NONE) +public final class DependencyGraphMojo extends BaseMojo { + + @Parameter(property = "versioning.relativePaths", required = true, defaultValue = "true") + boolean useRelativePaths = true; + + @Override + protected void internalExecute() throws MojoExecutionException, MojoFailureException { + Path executionRootDirectory = Path.of(session.getExecutionRootDirectory()); + Map graph = getProjectsInScope() + .collect(Collectors.toMap( + Utils::mavenProjectToArtifact, + project -> new Node( + Utils.mavenProjectToArtifact(project), + executionRootDirectory.relativize(project.getBasedir().toPath()) + ) + )); + + printDependencyGraph(null); + } + + private void printDependencyGraph(Object graph) throws MojoExecutionException { + try { + System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(graph)); + } catch (JsonProcessingException e) { + throw new MojoExecutionException("Failed to serialize dependency graph", e); + } + } + + public record Node(MavenArtifact artifact, Path folder) { + + public Node { + Objects.requireNonNull(artifact, "`artifact` must not be null"); + Objects.requireNonNull(folder, "`folder` must not be null"); + } + } +} diff --git a/src/main/java/io/github/bsels/semantic/version/UpdatePomMojo.java b/src/main/java/io/github/bsels/semantic/version/UpdatePomMojo.java index 1001fb6..681426c 100644 --- a/src/main/java/io/github/bsels/semantic/version/UpdatePomMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/UpdatePomMojo.java @@ -311,7 +311,7 @@ private int handleSingleProject(MarkdownMapping markdownMapping, MavenProject pr throws MojoExecutionException, MojoFailureException { Path pom = project.getFile() .toPath(); - MavenArtifact artifact = new MavenArtifact(project.getGroupId(), project.getArtifactId()); + MavenArtifact artifact = Utils.mavenProjectToArtifact(project); Document document = POMUtils.readPom(pom); diff --git a/src/main/java/io/github/bsels/semantic/version/VerifyMojo.java b/src/main/java/io/github/bsels/semantic/version/VerifyMojo.java index cfc3a75..b7eccfa 100644 --- a/src/main/java/io/github/bsels/semantic/version/VerifyMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/VerifyMojo.java @@ -7,6 +7,7 @@ import io.github.bsels.semantic.version.parameters.Git; import io.github.bsels.semantic.version.parameters.VerificationMode; import io.github.bsels.semantic.version.utils.ProcessUtils; +import io.github.bsels.semantic.version.utils.Utils; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; @@ -15,6 +16,7 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; import java.util.ArrayDeque; import java.util.HashSet; @@ -124,7 +126,7 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep } Set projects = getProjectsInScope() - .map(project -> new MavenArtifact(project.getGroupId(), project.getArtifactId())) + .map(Utils::mavenProjectToArtifact) .collect(Collectors.toSet()); List versionMarkdowns = getVersionMarkdowns(); diff --git a/src/main/java/io/github/bsels/semantic/version/utils/Utils.java b/src/main/java/io/github/bsels/semantic/version/utils/Utils.java index bce271f..51f9666 100644 --- a/src/main/java/io/github/bsels/semantic/version/utils/Utils.java +++ b/src/main/java/io/github/bsels/semantic/version/utils/Utils.java @@ -1,5 +1,6 @@ package io.github.bsels.semantic.version.utils; +import io.github.bsels.semantic.version.models.MavenArtifact; import io.github.bsels.semantic.version.models.PlaceHolderWithType; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; @@ -58,6 +59,10 @@ public final class Utils { /// dynamically replaceable placeholders for date and version values. private static final Pattern PLACEHOLDER_FORMAT_EXTRACTOR = Pattern.compile("\\{(date(#([^{}]*))?|version)}"); + /// A cached collection of DateTimeFormatter instances, where the key is a [String] representing + /// the date-time pattern and the value is a corresponding [DateTimeFormatter] object. + /// This map is used to optimize the creation of [DateTimeFormatter] objects by reusing previously created instances + /// for the same pattern, reducing the overhead of instantiating new formatters. private static final Map CACHED_DATE_FORMATTERS = new HashMap<>(); /// Utility class containing static constants and methods for various common operations. @@ -322,4 +327,12 @@ public static BinaryOperator consumerToOperator(BiConsumer public static Collector> asImmutableSet() { return Collectors.collectingAndThen(Collectors.toSet(), Set::copyOf); } + + /// Converts a [MavenProject] instance into a [MavenArtifact] instance. + /// + /// @param project the [MavenProject] to be converted; must provide valid group ID and artifact ID. + /// @return a [MavenArtifact] instance containing the group ID and artifact ID from the provided [MavenProject]. + public static MavenArtifact mavenProjectToArtifact(MavenProject project) { + return new MavenArtifact(project.getGroupId(), project.getArtifactId()); + } } From 0b5002200563ec3edf42d5c247802356d1f43a18 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Thu, 12 Feb 2026 20:05:10 +0100 Subject: [PATCH 02/21] Add MavenProjectToArtifact test and update .gitignore --- .gitignore | 3 ++- .../semantic/version/utils/UtilsTest.java | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b9abcea..8b262d9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ buildNumber.properties .classpath # JetBrains -.idea \ No newline at end of file +.idea +.junie \ No newline at end of file diff --git a/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java b/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java index 6da538e..ee69093 100644 --- a/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java +++ b/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java @@ -1,5 +1,6 @@ package io.github.bsels.semantic.version.utils; +import io.github.bsels.semantic.version.models.MavenArtifact; import io.github.bsels.semantic.version.models.PlaceHolderWithType; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; @@ -738,4 +739,22 @@ void unknownPlaceholder_Preserved() { assertThat(result).isEqualTo("Release {unknown} on 2024-02-03"); } } + + @Nested + class MavenProjectToArtifactTest { + + @Test + void validProject_ReturnsCorrectArtifact() { + Mockito.when(mavenProject.getGroupId()) + .thenReturn("io.github.bsels"); + Mockito.when(mavenProject.getArtifactId()) + .thenReturn("semantic-version-maven-plugin"); + + MavenArtifact artifact = Utils.mavenProjectToArtifact(mavenProject); + + assertThat(artifact).isNotNull(); + assertThat(artifact.groupId()).isEqualTo("io.github.bsels"); + assertThat(artifact.artifactId()).isEqualTo("semantic-version-maven-plugin"); + } + } } From 81952dda7749851925131edaf6b29210f6a0e4e9 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Thu, 12 Feb 2026 20:15:35 +0100 Subject: [PATCH 03/21] Add unit test for DependencyGraphMojo and enhance dependency graph generation logic --- .../semantic/version/DependencyGraphMojo.java | 34 ++++++++-- .../version/DependencyGraphMojoTest.java | 63 +++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java index 26a2897..8b8280a 100644 --- a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.github.bsels.semantic.version.models.MavenArtifact; +import io.github.bsels.semantic.version.models.MavenProjectAndDocument; import io.github.bsels.semantic.version.utils.Utils; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -11,10 +12,13 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; import java.nio.file.Path; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; @Mojo(name = "graph", aggregator = true, requiresDependencyResolution = ResolutionScope.NONE) @@ -27,19 +31,38 @@ public final class DependencyGraphMojo extends BaseMojo { @Override protected void internalExecute() throws MojoExecutionException, MojoFailureException { Path executionRootDirectory = Path.of(session.getExecutionRootDirectory()); - Map graph = getProjectsInScope() + List projectsInScope = getProjectsInScope().toList(); + Set projectArtifacts = projectsInScope.stream() + .map(Utils::mavenProjectToArtifact) + .collect(Collectors.toSet()); + + Map documents = readAllPoms(projectsInScope); + Map> dependencyToProjectArtifactMapping = + createDependencyToProjectArtifactMapping(documents.values(), projectArtifacts); + + Map> projectToDependenciesMapping = projectArtifacts.stream() + .collect(Collectors.toMap( + artifact -> artifact, + artifact -> dependencyToProjectArtifactMapping.entrySet().stream() + .filter(entry -> entry.getValue().contains(artifact)) + .map(Map.Entry::getKey) + .toList() + )); + + Map graph = projectsInScope.stream() .collect(Collectors.toMap( Utils::mavenProjectToArtifact, project -> new Node( Utils.mavenProjectToArtifact(project), - executionRootDirectory.relativize(project.getBasedir().toPath()) + executionRootDirectory.relativize(project.getBasedir().toPath()), + projectToDependenciesMapping.getOrDefault(Utils.mavenProjectToArtifact(project), List.of()) ) )); - printDependencyGraph(null); + printDependencyGraph(graph); } - private void printDependencyGraph(Object graph) throws MojoExecutionException { + void printDependencyGraph(Object graph) throws MojoExecutionException { try { System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(graph)); } catch (JsonProcessingException e) { @@ -47,11 +70,12 @@ private void printDependencyGraph(Object graph) throws MojoExecutionException { } } - public record Node(MavenArtifact artifact, Path folder) { + public record Node(MavenArtifact artifact, Path folder, List dependencies) { public Node { Objects.requireNonNull(artifact, "`artifact` must not be null"); Objects.requireNonNull(folder, "`folder` must not be null"); + Objects.requireNonNull(dependencies, "`dependencies` must not be null"); } } } diff --git a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java new file mode 100644 index 0000000..4dfe8c7 --- /dev/null +++ b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java @@ -0,0 +1,63 @@ +package io.github.bsels.semantic.version; + +import io.github.bsels.semantic.version.models.MavenArtifact; +import io.github.bsels.semantic.version.test.utils.ReadMockedMavenSession; +import io.github.bsels.semantic.version.test.utils.TestLog; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.nio.file.Path; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class DependencyGraphMojoTest extends AbstractBaseMojoTest { + + @Spy + private DependencyGraphMojo classUnderTest; + + @BeforeEach + void setUp() { + classUnderTest.setLog(new TestLog(TestLog.LogLevel.NONE)); + } + + @Test + void internalExecute_calculatesCorrectDependencies() throws MojoExecutionException, MojoFailureException { + // Arrange + Path projectRoot = getResourcesPath("multi"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + + ArgumentCaptor graphCaptor = ArgumentCaptor.forClass(Object.class); + doNothing().when(classUnderTest).printDependencyGraph(graphCaptor.capture()); + + // Act + classUnderTest.internalExecute(); + + // Assert + Map graph = (Map) graphCaptor.getValue(); + assertThat(graph).isNotNull(); + + MavenArtifact combinationArtifact = new MavenArtifact("org.example.itests.multi", "combination"); + assertThat(graph).containsKey(combinationArtifact); + + DependencyGraphMojo.Node combinationNode = graph.get(combinationArtifact); + assertThat(combinationNode.dependencies()).contains( + new MavenArtifact("org.example.itests.multi", "dependency"), + new MavenArtifact("org.example.itests.multi", "plugin") + ); + + MavenArtifact dependencyArtifact = new MavenArtifact("org.example.itests.multi", "dependency"); + assertThat(graph).containsKey(dependencyArtifact); + assertThat(graph.get(dependencyArtifact).dependencies()).isEmpty(); + } +} From d7c8c0ac0bfabab897d692084fcca5f0964b5fe9 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Thu, 12 Feb 2026 20:30:14 +0100 Subject: [PATCH 04/21] Refactor `DependencyGraphMojo` to support relative and absolute paths and adjust the dependency graph node structure --- .../semantic/version/DependencyGraphMojo.java | 22 ++++++++++++++++--- src/test/resources/itests/multi/pom.xml | 2 ++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java index 8b8280a..176f388 100644 --- a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -30,7 +30,9 @@ public final class DependencyGraphMojo extends BaseMojo { @Override protected void internalExecute() throws MojoExecutionException, MojoFailureException { - Path executionRootDirectory = Path.of(session.getExecutionRootDirectory()); + Path executionRootDirectory = Path.of(session.getExecutionRootDirectory()) + .toAbsolutePath(); + System.out.println(executionRootDirectory); List projectsInScope = getProjectsInScope().toList(); Set projectArtifacts = projectsInScope.stream() .map(Utils::mavenProjectToArtifact) @@ -54,14 +56,28 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep Utils::mavenProjectToArtifact, project -> new Node( Utils.mavenProjectToArtifact(project), - executionRootDirectory.relativize(project.getBasedir().toPath()), + getProjectFolderAsString(project, executionRootDirectory), projectToDependenciesMapping.getOrDefault(Utils.mavenProjectToArtifact(project), List.of()) ) )); + // TODO Improve structure to needs + printDependencyGraph(graph); } + private String getProjectFolderAsString(MavenProject project, Path executionRootDirectory) { + Path projectBasePath = project.getBasedir().toPath().toAbsolutePath(); + if (!useRelativePaths) { + return projectBasePath.toString(); + } + String relativePath = executionRootDirectory.relativize(projectBasePath).toString(); + if (relativePath.isBlank()) { + return "."; + } + return relativePath; + } + void printDependencyGraph(Object graph) throws MojoExecutionException { try { System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(graph)); @@ -70,7 +86,7 @@ void printDependencyGraph(Object graph) throws MojoExecutionException { } } - public record Node(MavenArtifact artifact, Path folder, List dependencies) { + public record Node(MavenArtifact artifact, String folder, List dependencies) { public Node { Objects.requireNonNull(artifact, "`artifact` must not be null"); diff --git a/src/test/resources/itests/multi/pom.xml b/src/test/resources/itests/multi/pom.xml index ce48418..56b52d6 100644 --- a/src/test/resources/itests/multi/pom.xml +++ b/src/test/resources/itests/multi/pom.xml @@ -7,6 +7,8 @@ parent 4.0.0-parent + pom + dependency dependencyManagement From 74596519e8467634b62517818512e042e8cab5f8 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Fri, 13 Feb 2026 18:56:28 +0100 Subject: [PATCH 05/21] Enhance `DependencyGraphMojo` structure by introducing `MinDependency` and updating node dependencies mapping logic --- .../semantic/version/DependencyGraphMojo.java | 31 +++++++++++++++---- .../version/DependencyGraphMojoTest.java | 4 +-- .../semantic/version/UpdatePomMojoTest.java | 2 ++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java index 176f388..9dc4c54 100644 --- a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -51,14 +51,26 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep .toList() )); + Map artifactToFolderMapping = projectsInScope.stream() + .collect(Collectors.toMap( + Utils::mavenProjectToArtifact, + project -> getProjectFolderAsString(project, executionRootDirectory) + )); + Map graph = projectsInScope.stream() .collect(Collectors.toMap( Utils::mavenProjectToArtifact, - project -> new Node( - Utils.mavenProjectToArtifact(project), - getProjectFolderAsString(project, executionRootDirectory), - projectToDependenciesMapping.getOrDefault(Utils.mavenProjectToArtifact(project), List.of()) - ) + project -> { + MavenArtifact artifact = Utils.mavenProjectToArtifact(project); + List minDependencies = projectToDependenciesMapping.getOrDefault(artifact, List.of()).stream() + .map(depArtifact -> new MinDependency(depArtifact, artifactToFolderMapping.get(depArtifact))) + .toList(); + return new Node( + artifact, + artifactToFolderMapping.get(artifact), + minDependencies + ); + } )); // TODO Improve structure to needs @@ -86,7 +98,14 @@ void printDependencyGraph(Object graph) throws MojoExecutionException { } } - public record Node(MavenArtifact artifact, String folder, List dependencies) { + public record MinDependency(MavenArtifact artifact, String folder) { + public MinDependency { + Objects.requireNonNull(artifact, "`artifact` must not be null"); + Objects.requireNonNull(folder, "`folder` must not be null"); + } + } + + public record Node(MavenArtifact artifact, String folder, List dependencies) { public Node { Objects.requireNonNull(artifact, "`artifact` must not be null"); diff --git a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java index 4dfe8c7..5ea8668 100644 --- a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java +++ b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java @@ -52,8 +52,8 @@ void internalExecute_calculatesCorrectDependencies() throws MojoExecutionExcepti DependencyGraphMojo.Node combinationNode = graph.get(combinationArtifact); assertThat(combinationNode.dependencies()).contains( - new MavenArtifact("org.example.itests.multi", "dependency"), - new MavenArtifact("org.example.itests.multi", "plugin") + new DependencyGraphMojo.MinDependency(new MavenArtifact("org.example.itests.multi", "dependency"), "dependency"), + new DependencyGraphMojo.MinDependency(new MavenArtifact("org.example.itests.multi", "plugin"), "plugin") ); MavenArtifact dependencyArtifact = new MavenArtifact("org.example.itests.multi", "dependency"); diff --git a/src/test/java/io/github/bsels/semantic/version/UpdatePomMojoTest.java b/src/test/java/io/github/bsels/semantic/version/UpdatePomMojoTest.java index 634031a..4382554 100644 --- a/src/test/java/io/github/bsels/semantic/version/UpdatePomMojoTest.java +++ b/src/test/java/io/github/bsels/semantic/version/UpdatePomMojoTest.java @@ -1110,6 +1110,8 @@ void handleDependencyCorrect_NoErrors( %1$s 4.1.0-%1$s + pom + dependency dependencyManagement From 452189ddad9be8577477945f029059447606d98c Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Fri, 13 Feb 2026 19:10:31 +0100 Subject: [PATCH 06/21] Refactor `DependencyGraphMojo` to streamline dependency mapping and enhance JSON serialization logic --- .../semantic/version/DependencyGraphMojo.java | 38 ++++++------------- .../bsels/semantic/version/utils/Utils.java | 37 ++++++++++++++++++ .../version/DependencyGraphMojoTest.java | 19 +--------- 3 files changed, 49 insertions(+), 45 deletions(-) diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java index 9dc4c54..8927469 100644 --- a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -1,7 +1,5 @@ package io.github.bsels.semantic.version; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.github.bsels.semantic.version.models.MavenArtifact; import io.github.bsels.semantic.version.models.MavenProjectAndDocument; import io.github.bsels.semantic.version.utils.Utils; @@ -18,7 +16,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; @Mojo(name = "graph", aggregator = true, requiresDependencyResolution = ResolutionScope.NONE) @@ -34,15 +31,18 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep .toAbsolutePath(); System.out.println(executionRootDirectory); List projectsInScope = getProjectsInScope().toList(); - Set projectArtifacts = projectsInScope.stream() - .map(Utils::mavenProjectToArtifact) - .collect(Collectors.toSet()); + Map projectArtifacts = projectsInScope.stream() + .collect(Collectors.toMap( + Utils::mavenProjectToArtifact, + project -> getProjectFolderAsString(project, executionRootDirectory) + )); Map documents = readAllPoms(projectsInScope); Map> dependencyToProjectArtifactMapping = - createDependencyToProjectArtifactMapping(documents.values(), projectArtifacts); + createDependencyToProjectArtifactMapping(documents.values(), projectArtifacts.keySet()); - Map> projectToDependenciesMapping = projectArtifacts.stream() + Map> projectToDependenciesMapping = projectArtifacts.keySet() + .stream() .collect(Collectors.toMap( artifact -> artifact, artifact -> dependencyToProjectArtifactMapping.entrySet().stream() @@ -51,31 +51,23 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep .toList() )); - Map artifactToFolderMapping = projectsInScope.stream() - .collect(Collectors.toMap( - Utils::mavenProjectToArtifact, - project -> getProjectFolderAsString(project, executionRootDirectory) - )); - Map graph = projectsInScope.stream() .collect(Collectors.toMap( Utils::mavenProjectToArtifact, project -> { MavenArtifact artifact = Utils.mavenProjectToArtifact(project); List minDependencies = projectToDependenciesMapping.getOrDefault(artifact, List.of()).stream() - .map(depArtifact -> new MinDependency(depArtifact, artifactToFolderMapping.get(depArtifact))) + .map(depArtifact -> new MinDependency(depArtifact, projectArtifacts.get(depArtifact))) .toList(); return new Node( artifact, - artifactToFolderMapping.get(artifact), + projectArtifacts.get(artifact), minDependencies ); } )); - // TODO Improve structure to needs - - printDependencyGraph(graph); + System.out.println(Utils.writeObjectAsJson(graph)); } private String getProjectFolderAsString(MavenProject project, Path executionRootDirectory) { @@ -90,14 +82,6 @@ private String getProjectFolderAsString(MavenProject project, Path executionRoot return relativePath; } - void printDependencyGraph(Object graph) throws MojoExecutionException { - try { - System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(graph)); - } catch (JsonProcessingException e) { - throw new MojoExecutionException("Failed to serialize dependency graph", e); - } - } - public record MinDependency(MavenArtifact artifact, String folder) { public MinDependency { Objects.requireNonNull(artifact, "`artifact` must not be null"); diff --git a/src/main/java/io/github/bsels/semantic/version/utils/Utils.java b/src/main/java/io/github/bsels/semantic/version/utils/Utils.java index 51f9666..f9e60de 100644 --- a/src/main/java/io/github/bsels/semantic/version/utils/Utils.java +++ b/src/main/java/io/github/bsels/semantic/version/utils/Utils.java @@ -1,7 +1,14 @@ package io.github.bsels.semantic.version.utils; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; import io.github.bsels.semantic.version.models.MavenArtifact; import io.github.bsels.semantic.version.models.PlaceHolderWithType; +import io.github.bsels.semantic.version.utils.mapper.MavenArtifactArtifactOnlySerializer; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; @@ -65,6 +72,22 @@ public final class Utils { /// for the same pattern, reducing the overhead of instantiating new formatters. private static final Map CACHED_DATE_FORMATTERS = new HashMap<>(); + /// A statically initialized instance of [ObjectMapper] configured with custom serializers. + /// This instance is designed to handle serialization of [MavenArtifact] objects, + /// where both a key serializer and a general serializer are registered. + /// The custom serializers are provided by [MavenArtifactArtifactOnlySerializer]. + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .registerModule( + new SimpleModule() + .addKeySerializer(MavenArtifact.class, new JsonSerializer() { + @Override + public void serialize(MavenArtifact value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeFieldName(value.artifactId()); + } + }) + .addSerializer(MavenArtifact.class, new MavenArtifactArtifactOnlySerializer()) + ); + /// Utility class containing static constants and methods for various common operations. /// This class is not designed to be instantiated. private Utils() { @@ -335,4 +358,18 @@ public static BinaryOperator consumerToOperator(BiConsumer public static MavenArtifact mavenProjectToArtifact(MavenProject project) { return new MavenArtifact(project.getGroupId(), project.getArtifactId()); } + + /// Serializes the given object into a JSON-formatted string. + /// + /// @param object the object to be serialized into JSON + /// @return a JSON-formatted string representation of the given object + /// @throws MojoExecutionException if an error occurs during serialization + public static String writeObjectAsJson(Object object) throws MojoExecutionException { + try { + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter() + .writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new MojoExecutionException("Failed to serialize object to JSON", e); + } + } } diff --git a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java index 5ea8668..71676aa 100644 --- a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java +++ b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java @@ -36,28 +36,11 @@ void internalExecute_calculatesCorrectDependencies() throws MojoExecutionExcepti // Arrange Path projectRoot = getResourcesPath("multi"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); - - ArgumentCaptor graphCaptor = ArgumentCaptor.forClass(Object.class); - doNothing().when(classUnderTest).printDependencyGraph(graphCaptor.capture()); // Act classUnderTest.internalExecute(); // Assert - Map graph = (Map) graphCaptor.getValue(); - assertThat(graph).isNotNull(); - - MavenArtifact combinationArtifact = new MavenArtifact("org.example.itests.multi", "combination"); - assertThat(graph).containsKey(combinationArtifact); - - DependencyGraphMojo.Node combinationNode = graph.get(combinationArtifact); - assertThat(combinationNode.dependencies()).contains( - new DependencyGraphMojo.MinDependency(new MavenArtifact("org.example.itests.multi", "dependency"), "dependency"), - new DependencyGraphMojo.MinDependency(new MavenArtifact("org.example.itests.multi", "plugin"), "plugin") - ); - - MavenArtifact dependencyArtifact = new MavenArtifact("org.example.itests.multi", "dependency"); - assertThat(graph).containsKey(dependencyArtifact); - assertThat(graph.get(dependencyArtifact).dependencies()).isEmpty(); + verify(classUnderTest).internalExecute(); } } From 935f1ea3b141b56ed3967680afb8c1dcb058ae40 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Fri, 13 Feb 2026 19:17:27 +0100 Subject: [PATCH 07/21] Add unit tests for JSON serialization and improve artifact serializer logic --- .../MavenArtifactArtifactOnlySerializer.java | 4 +- .../semantic/version/utils/UtilsTest.java | 45 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/bsels/semantic/version/utils/mapper/MavenArtifactArtifactOnlySerializer.java b/src/main/java/io/github/bsels/semantic/version/utils/mapper/MavenArtifactArtifactOnlySerializer.java index e78d282..6cdf8bf 100644 --- a/src/main/java/io/github/bsels/semantic/version/utils/mapper/MavenArtifactArtifactOnlySerializer.java +++ b/src/main/java/io/github/bsels/semantic/version/utils/mapper/MavenArtifactArtifactOnlySerializer.java @@ -1,6 +1,7 @@ package io.github.bsels.semantic.version.utils.mapper; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonStreamContext; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import io.github.bsels.semantic.version.models.MavenArtifact; @@ -47,7 +48,8 @@ public void serialize( JsonGenerator jsonGenerator, SerializerProvider serializerProvider ) throws IOException { - if (jsonGenerator.getOutputContext().hasCurrentName()) { + JsonStreamContext outputContext = jsonGenerator.getOutputContext(); + if (outputContext.hasCurrentName() || outputContext.getParent() == null) { jsonGenerator.writeString(mavenArtifact.artifactId()); } else { jsonGenerator.writeFieldName(mavenArtifact.artifactId()); diff --git a/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java b/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java index ee69093..9e20cec 100644 --- a/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java +++ b/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java @@ -1,5 +1,6 @@ package io.github.bsels.semantic.version.utils; +import com.fasterxml.jackson.core.JsonProcessingException; import io.github.bsels.semantic.version.models.MavenArtifact; import io.github.bsels.semantic.version.models.PlaceHolderWithType; import org.apache.maven.plugin.MojoExecutionException; @@ -751,10 +752,52 @@ void validProject_ReturnsCorrectArtifact() { .thenReturn("semantic-version-maven-plugin"); MavenArtifact artifact = Utils.mavenProjectToArtifact(mavenProject); - assertThat(artifact).isNotNull(); assertThat(artifact.groupId()).isEqualTo("io.github.bsels"); assertThat(artifact.artifactId()).isEqualTo("semantic-version-maven-plugin"); } } + + @Nested + class WriteObjectAsJsonTest { + + @Test + void validObject_ReturnsJson() throws MojoExecutionException { + Map map = Map.of("key", "value"); + String json = Utils.writeObjectAsJson(map); + + assertThat(json).contains("\"key\" : \"value\""); + } + + @Test + void mavenArtifact_ReturnsJsonWithArtifactIdOnly() throws MojoExecutionException { + MavenArtifact artifact = new MavenArtifact("io.github.bsels", "semantic-version-maven-plugin"); + String json = Utils.writeObjectAsJson(artifact); + + assertThat(json).isEqualTo("\"semantic-version-maven-plugin\""); + } + + @Test + void mavenArtifactAsKey_ReturnsJsonWithArtifactIdAsKey() throws MojoExecutionException { + MavenArtifact artifact = new MavenArtifact("io.github.bsels", "semantic-version-maven-plugin"); + Map map = Map.of(artifact, "value"); + String json = Utils.writeObjectAsJson(map); + + assertThat(json).contains("\"semantic-version-maven-plugin\" : \"value\""); + } + + @Test + void failingObject_ThrowsMojoExecutionException() { + Object failingObject = new Object() { + public String getFailingProperty() { + throw new RuntimeException("Failing getter"); + } + }; + + assertThatThrownBy(() -> Utils.writeObjectAsJson(failingObject)) + .isInstanceOf(MojoExecutionException.class) + .hasMessage("Failed to serialize object to JSON") + .hasCauseInstanceOf(JsonProcessingException.class); + } + } } From de4c27472cf83217143425122c2eb57fc96ca2bf Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Fri, 13 Feb 2026 19:29:49 +0100 Subject: [PATCH 08/21] Refactor `DependencyGraphMojo` to improve dependency mapping logic by introducing helper methods for node preparation and dependency collection --- .../semantic/version/DependencyGraphMojo.java | 77 ++++++++++++++----- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java index 8927469..e437595 100644 --- a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; @Mojo(name = "graph", aggregator = true, requiresDependencyResolution = ResolutionScope.NONE) @@ -29,7 +30,7 @@ public final class DependencyGraphMojo extends BaseMojo { protected void internalExecute() throws MojoExecutionException, MojoFailureException { Path executionRootDirectory = Path.of(session.getExecutionRootDirectory()) .toAbsolutePath(); - System.out.println(executionRootDirectory); + getLog().info("Execution root directory: %s".formatted(executionRootDirectory)); List projectsInScope = getProjectsInScope().toList(); Map projectArtifacts = projectsInScope.stream() .collect(Collectors.toMap( @@ -44,32 +45,48 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep Map> projectToDependenciesMapping = projectArtifacts.keySet() .stream() .collect(Collectors.toMap( - artifact -> artifact, - artifact -> dependencyToProjectArtifactMapping.entrySet().stream() - .filter(entry -> entry.getValue().contains(artifact)) - .map(Map.Entry::getKey) - .toList() + Function.identity(), + artifact -> collectProjectDependencies(artifact, dependencyToProjectArtifactMapping) )); - Map graph = projectsInScope.stream() + Map graph = projectArtifacts.keySet() + .stream() .collect(Collectors.toMap( - Utils::mavenProjectToArtifact, - project -> { - MavenArtifact artifact = Utils.mavenProjectToArtifact(project); - List minDependencies = projectToDependenciesMapping.getOrDefault(artifact, List.of()).stream() - .map(depArtifact -> new MinDependency(depArtifact, projectArtifacts.get(depArtifact))) - .toList(); - return new Node( - artifact, - projectArtifacts.get(artifact), - minDependencies - ); - } + Function.identity(), + project -> prepareMavenProjectNode(project, projectToDependenciesMapping, projectArtifacts) )); System.out.println(Utils.writeObjectAsJson(graph)); } + /// Resolves and collects all Maven artifacts that are dependent on the specified artifact within the provided + /// dependency mapping. + /// + /// This method filters through the `dependencyToProjectArtifactMapping` to find all entries + /// where the given `artifact` is present in the list of dependencies, + /// and then retrieves the corresponding project artifacts as a result. + /// + /// @param artifact the Maven artifact whose dependents need to be collected + /// @param dependencyToProjectArtifactMapping a mapping that associates project artifacts with their dependencies + /// @return a list of Maven artifacts that depend on the specified artifact + private List collectProjectDependencies( + MavenArtifact artifact, + Map> dependencyToProjectArtifactMapping + ) { + return dependencyToProjectArtifactMapping.entrySet() + .stream() + .filter(entry -> entry.getValue().contains(artifact)) + .map(Map.Entry::getKey) + .toList(); + } + + /// Returns the project folder path as a string. + /// If relative paths are enabled, the path is returned relative to the specified execution root directory; + /// otherwise, the absolute path of the project is returned. + /// + /// @param project the [MavenProject] instance representing the current project + /// @param executionRootDirectory the root directory of the current execution context + /// @return the project folder path as a string, either relative to the execution root directory or as an absolute path private String getProjectFolderAsString(MavenProject project, Path executionRootDirectory) { Path projectBasePath = project.getBasedir().toPath().toAbsolutePath(); if (!useRelativePaths) { @@ -82,6 +99,28 @@ private String getProjectFolderAsString(MavenProject project, Path executionRoot return relativePath; } + /// Prepares a `Node` representation of a Maven project artifact, including its associated + /// folder path and its minimal dependencies as `MinDependency` instances. + /// + /// @param artifact the Maven artifact for which the `Node` is to be prepared; must not be null + /// @param projectToDependenciesMapping a mapping of Maven projects to their respective dependencies; used to determine the dependencies of the provided artifact; must not be null + /// @param projectArtifacts a mapping of Maven artifacts to their corresponding folder paths; must not be null + /// @return a `Node` instance representing the given artifact, its folder path, and its resolved dependencies + private Node prepareMavenProjectNode( + MavenArtifact artifact, + Map> projectToDependenciesMapping, + Map projectArtifacts + ) { + List minDependencies = projectToDependenciesMapping.getOrDefault(artifact, List.of()).stream() + .map(depArtifact -> new MinDependency(depArtifact, projectArtifacts.get(depArtifact))) + .toList(); + return new Node( + artifact, + projectArtifacts.get(artifact), + minDependencies + ); + } + public record MinDependency(MavenArtifact artifact, String folder) { public MinDependency { Objects.requireNonNull(artifact, "`artifact` must not be null"); From ac5ab5b33e774e2c8d05ec7dafbb5e127e016584 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 14:51:19 +0100 Subject: [PATCH 09/21] Add integration tests for chained dependencies and refactor artifact serialization logic --- .../bsels/semantic/version/utils/Utils.java | 12 +--------- .../semantic/version/utils/UtilsTest.java | 13 +++++++---- .../itests/chained-dependency/a/CHANGELOG.md | 5 +++++ .../itests/chained-dependency/a/pom.xml | 9 ++++++++ .../itests/chained-dependency/b/CHANGELOG.md | 5 +++++ .../itests/chained-dependency/b/pom.xml | 17 ++++++++++++++ .../itests/chained-dependency/c/CHANGELOG.md | 5 +++++ .../itests/chained-dependency/c/pom.xml | 17 ++++++++++++++ .../itests/chained-dependency/d/CHANGELOG.md | 5 +++++ .../itests/chained-dependency/d/pom.xml | 22 +++++++++++++++++++ .../itests/chained-dependency/e/CHANGELOG.md | 5 +++++ .../itests/chained-dependency/e/pom.xml | 17 ++++++++++++++ .../itests/chained-dependency/pom.xml | 19 ++++++++++++++++ 13 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 src/test/resources/itests/chained-dependency/a/CHANGELOG.md create mode 100644 src/test/resources/itests/chained-dependency/a/pom.xml create mode 100644 src/test/resources/itests/chained-dependency/b/CHANGELOG.md create mode 100644 src/test/resources/itests/chained-dependency/b/pom.xml create mode 100644 src/test/resources/itests/chained-dependency/c/CHANGELOG.md create mode 100644 src/test/resources/itests/chained-dependency/c/pom.xml create mode 100644 src/test/resources/itests/chained-dependency/d/CHANGELOG.md create mode 100644 src/test/resources/itests/chained-dependency/d/pom.xml create mode 100644 src/test/resources/itests/chained-dependency/e/CHANGELOG.md create mode 100644 src/test/resources/itests/chained-dependency/e/pom.xml create mode 100644 src/test/resources/itests/chained-dependency/pom.xml diff --git a/src/main/java/io/github/bsels/semantic/version/utils/Utils.java b/src/main/java/io/github/bsels/semantic/version/utils/Utils.java index f9e60de..226cd12 100644 --- a/src/main/java/io/github/bsels/semantic/version/utils/Utils.java +++ b/src/main/java/io/github/bsels/semantic/version/utils/Utils.java @@ -76,17 +76,7 @@ public final class Utils { /// This instance is designed to handle serialization of [MavenArtifact] objects, /// where both a key serializer and a general serializer are registered. /// The custom serializers are provided by [MavenArtifactArtifactOnlySerializer]. - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .registerModule( - new SimpleModule() - .addKeySerializer(MavenArtifact.class, new JsonSerializer() { - @Override - public void serialize(MavenArtifact value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeFieldName(value.artifactId()); - } - }) - .addSerializer(MavenArtifact.class, new MavenArtifactArtifactOnlySerializer()) - ); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /// Utility class containing static constants and methods for various common operations. /// This class is not designed to be instantiated. diff --git a/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java b/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java index 9e20cec..4b2356b 100644 --- a/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java +++ b/src/test/java/io/github/bsels/semantic/version/utils/UtilsTest.java @@ -770,20 +770,25 @@ void validObject_ReturnsJson() throws MojoExecutionException { } @Test - void mavenArtifact_ReturnsJsonWithArtifactIdOnly() throws MojoExecutionException { + void mavenArtifact_ReturnsJson() throws MojoExecutionException { MavenArtifact artifact = new MavenArtifact("io.github.bsels", "semantic-version-maven-plugin"); String json = Utils.writeObjectAsJson(artifact); - assertThat(json).isEqualTo("\"semantic-version-maven-plugin\""); + assertThat(json).isEqualToIgnoringNewLines(""" + { + "groupId" : "io.github.bsels", + "artifactId" : "semantic-version-maven-plugin" + } + """); } @Test - void mavenArtifactAsKey_ReturnsJsonWithArtifactIdAsKey() throws MojoExecutionException { + void mavenArtifactAsKey_ReturnsJsonWithMavenArtifactAsKey() throws MojoExecutionException { MavenArtifact artifact = new MavenArtifact("io.github.bsels", "semantic-version-maven-plugin"); Map map = Map.of(artifact, "value"); String json = Utils.writeObjectAsJson(map); - assertThat(json).contains("\"semantic-version-maven-plugin\" : \"value\""); + assertThat(json).contains("\"io.github.bsels:semantic-version-maven-plugin\" : \"value\""); } @Test diff --git a/src/test/resources/itests/chained-dependency/a/CHANGELOG.md b/src/test/resources/itests/chained-dependency/a/CHANGELOG.md new file mode 100644 index 0000000..26a604f --- /dev/null +++ b/src/test/resources/itests/chained-dependency/a/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 6.0.0-a - 2026-01-01 + +Initial a release. \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/a/pom.xml b/src/test/resources/itests/chained-dependency/a/pom.xml new file mode 100644 index 0000000..413989b --- /dev/null +++ b/src/test/resources/itests/chained-dependency/a/pom.xml @@ -0,0 +1,9 @@ + + + 4.0.0 + org.example.itests.chained + a + 6.0.0-a + \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/b/CHANGELOG.md b/src/test/resources/itests/chained-dependency/b/CHANGELOG.md new file mode 100644 index 0000000..9a0cccd --- /dev/null +++ b/src/test/resources/itests/chained-dependency/b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 6.0.0-b - 2026-01-01 + +Initial b release. \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/b/pom.xml b/src/test/resources/itests/chained-dependency/b/pom.xml new file mode 100644 index 0000000..6ed15e3 --- /dev/null +++ b/src/test/resources/itests/chained-dependency/b/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + org.example.itests.chained + b + 6.0.0-b + + + + org.example.itests.chained + a + 6.0.0-a + + + \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/c/CHANGELOG.md b/src/test/resources/itests/chained-dependency/c/CHANGELOG.md new file mode 100644 index 0000000..cf78217 --- /dev/null +++ b/src/test/resources/itests/chained-dependency/c/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 6.0.0-c - 2026-01-01 + +Initial c release. \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/c/pom.xml b/src/test/resources/itests/chained-dependency/c/pom.xml new file mode 100644 index 0000000..d4128c3 --- /dev/null +++ b/src/test/resources/itests/chained-dependency/c/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + org.example.itests.chained + c + 6.0.0-c + + + + org.example.itests.chained + a + 6.0.0-a + + + \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/d/CHANGELOG.md b/src/test/resources/itests/chained-dependency/d/CHANGELOG.md new file mode 100644 index 0000000..619e649 --- /dev/null +++ b/src/test/resources/itests/chained-dependency/d/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 6.0.0-d - 2026-01-01 + +Initial d release. \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/d/pom.xml b/src/test/resources/itests/chained-dependency/d/pom.xml new file mode 100644 index 0000000..52f9cd4 --- /dev/null +++ b/src/test/resources/itests/chained-dependency/d/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + org.example.itests.chained + d + 6.0.0-d + + + + org.example.itests.chained + b + 6.0.0-b + + + org.example.itests.chained + c + 6.0.0-c + + + \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/e/CHANGELOG.md b/src/test/resources/itests/chained-dependency/e/CHANGELOG.md new file mode 100644 index 0000000..460bc07 --- /dev/null +++ b/src/test/resources/itests/chained-dependency/e/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 6.0.0-e - 2026-01-01 + +Initial e release. \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/e/pom.xml b/src/test/resources/itests/chained-dependency/e/pom.xml new file mode 100644 index 0000000..c9b1416 --- /dev/null +++ b/src/test/resources/itests/chained-dependency/e/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + org.example.itests.chained + e + 6.0.0-e + + + + org.example.itests.chained + d + 6.0.0-d + + + \ No newline at end of file diff --git a/src/test/resources/itests/chained-dependency/pom.xml b/src/test/resources/itests/chained-dependency/pom.xml new file mode 100644 index 0000000..add4a9c --- /dev/null +++ b/src/test/resources/itests/chained-dependency/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.example.itests.chained + root + 6.0.0-root + + pom + + + a + b + c + d + e + + \ No newline at end of file From 51cc1bea5687ece4867830e9dd2f981cfa96a5e4 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 15:08:58 +0100 Subject: [PATCH 10/21] Refactor `DependencyGraphMojo` to enhance dependency graph generation with transitive dependency resolution and relative path handling --- .../semantic/version/DependencyGraphMojo.java | 76 +++++++++++++++++-- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java index e437595..6ec43e5 100644 --- a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -13,9 +13,12 @@ import org.apache.maven.project.MavenProject; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -23,14 +26,34 @@ @Execute(phase = LifecyclePhase.NONE) public final class DependencyGraphMojo extends BaseMojo { + /// Indicates whether project folder paths should be resolved as relative paths with respect to + /// the execution root directory. + /// If set to `true`, the project paths will be returned as relative paths. + /// If set to `false`, absolute paths will be used instead. + /// This parameter is required and defaults to `true`. + /// + /// Configurable via the Maven property `versioning.relativePaths`. @Parameter(property = "versioning.relativePaths", required = true, defaultValue = "true") boolean useRelativePaths = true; + /// Executes the internal logic for creating a dependency graph representation of Maven projects in the current scope. + /// This method performs the following steps: + /// 1. Resolves the execution root directory and logs it for debugging. + /// 2. Retrieves all Maven projects in the current execution scope. + /// 3. Maps Maven projects to their corresponding artifacts and project folder paths. + /// 4. Parses and reads all POM files for the projects, creating a mapping of Maven artifacts to their parsed documents. + /// 5. Creates a dependency mapping for artifacts, associating project artifacts with their dependent artifacts. + /// 6. Resolves transitive dependencies for each project artifact and creates a mapping of project artifacts to their resolved dependencies. + /// 7. Prepares a directed graph representation of Maven project artifacts (`Node` instances) using the resolved dependency data. + /// 8. Produces the output of the dependency graph. + /// + /// @throws MojoExecutionException if an unexpected error occurs during execution. + /// @throws MojoFailureException if the execution fails due to inconsistent or invalid Maven project data. @Override protected void internalExecute() throws MojoExecutionException, MojoFailureException { Path executionRootDirectory = Path.of(session.getExecutionRootDirectory()) .toAbsolutePath(); - getLog().info("Execution root directory: %s".formatted(executionRootDirectory)); + getLog().debug("Execution root directory: %s".formatted(executionRootDirectory)); List projectsInScope = getProjectsInScope().toList(); Map projectArtifacts = projectsInScope.stream() .collect(Collectors.toMap( @@ -56,28 +79,67 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep project -> prepareMavenProjectNode(project, projectToDependenciesMapping, projectArtifacts) )); + produceGraphOutput(graph); + } + + private void produceGraphOutput(Map graph) throws MojoExecutionException { + // TODO: Enhance with different outputs and switching between console and file output System.out.println(Utils.writeObjectAsJson(graph)); } /// Resolves and collects all Maven artifacts that are dependent on the specified artifact within the provided - /// dependency mapping. + /// dependency mapping, including transitive dependencies. /// - /// This method filters through the `dependencyToProjectArtifactMapping` to find all entries - /// where the given `artifact` is present in the list of dependencies, - /// and then retrieves the corresponding project artifacts as a result. + /// This method performs a depth-first traversal to find all direct and transitive dependencies + /// of the given artifact. The dependencies are returned in topological order (build order), + /// where dependencies that need to be built first appear earlier in the list. /// /// @param artifact the Maven artifact whose dependents need to be collected /// @param dependencyToProjectArtifactMapping a mapping that associates project artifacts with their dependencies - /// @return a list of Maven artifacts that depend on the specified artifact + /// @return a list of Maven artifacts that depend on the specified artifact, sorted in build order private List collectProjectDependencies( MavenArtifact artifact, Map> dependencyToProjectArtifactMapping ) { - return dependencyToProjectArtifactMapping.entrySet() + Set visited = new HashSet<>(); + List result = new ArrayList<>(); + collectTransitiveDependencies(artifact, dependencyToProjectArtifactMapping, visited, result); + return List.copyOf(result); + } + + /// Recursively collects transitive dependencies using depth-first search. + /// Dependencies are added to the result list in post-order (dependencies before dependents), + /// which ensures topological ordering for build purposes. + /// + /// @param artifact the current artifact being processed + /// @param dependencyToProjectArtifactMapping a mapping that associates project artifacts with their dependencies + /// @param visited set of already visited artifacts to avoid cycles + /// @param result list to accumulate dependencies in topological order + private void collectTransitiveDependencies( + MavenArtifact artifact, + Map> dependencyToProjectArtifactMapping, + Set visited, + List result + ) { + if (visited.contains(artifact)) { + return; + } + visited.add(artifact); + + // Find all direct dependencies of this artifact + List directDependencies = dependencyToProjectArtifactMapping.entrySet() .stream() .filter(entry -> entry.getValue().contains(artifact)) .map(Map.Entry::getKey) .toList(); + + // Recursively process each direct dependency + for (MavenArtifact dependency : directDependencies) { + collectTransitiveDependencies(dependency, dependencyToProjectArtifactMapping, visited, result); + } + + // Add the current artifact after its dependencies (post-order) + result.add(artifact); } /// Returns the project folder path as a string. From 7ea5f05ad187d75dc884f2d1610e6bf4c0bb9735 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 15:53:51 +0100 Subject: [PATCH 11/21] Refactor `DependencyGraphMojo` to support configurable graph output formats and file-based output handling while introducing new data models for detailed dependency nodes --- .../semantic/version/DependencyGraphMojo.java | 140 ++++++++++++++---- .../models/graph/ArtifactLocation.java | 12 ++ .../models/graph/DetailedGraphNode.java | 15 ++ .../version/models/graph/package-info.java | 5 + .../semantic/version/models/package-info.java | 5 + .../bsels/semantic/version/package-info.java | 5 +- .../version/parameters/GraphOutput.java | 14 ++ .../version/parameters/package-info.java | 5 +- .../version/utils/mapper/package-info.java | 5 +- .../semantic/version/utils/package-info.java | 8 +- .../utils/yaml/front/block/package-info.java | 5 +- 11 files changed, 189 insertions(+), 30 deletions(-) create mode 100644 src/main/java/io/github/bsels/semantic/version/models/graph/ArtifactLocation.java create mode 100644 src/main/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNode.java create mode 100644 src/main/java/io/github/bsels/semantic/version/models/graph/package-info.java create mode 100644 src/main/java/io/github/bsels/semantic/version/models/package-info.java create mode 100644 src/main/java/io/github/bsels/semantic/version/parameters/GraphOutput.java diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java index 6ec43e5..56de066 100644 --- a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -2,6 +2,9 @@ import io.github.bsels.semantic.version.models.MavenArtifact; import io.github.bsels.semantic.version.models.MavenProjectAndDocument; +import io.github.bsels.semantic.version.models.graph.ArtifactLocation; +import io.github.bsels.semantic.version.models.graph.DetailedGraphNode; +import io.github.bsels.semantic.version.parameters.GraphOutput; import io.github.bsels.semantic.version.utils.Utils; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -12,12 +15,16 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -36,6 +43,54 @@ public final class DependencyGraphMojo extends BaseMojo { @Parameter(property = "versioning.relativePaths", required = true, defaultValue = "true") boolean useRelativePaths = true; + /// Specifies the format in which the dependency graph will be produced. + /// + /// This variable determines whether the dependency graph output includes: + /// - Only Maven artifact data ([GraphOutput#ARTIFACT_ONLY]), format: + /// ```json + /// { + /// "{groupId}:{artifactId}": ["{groupId}:{artifactId}",...], + /// ... + /// } + /// ``` + /// - Only the folder paths of the projects ([GraphOutput#FOLDER_ONLY]), format + /// ```json + /// { + /// "{groupId}:{artifactId}": ["{folder}",...], + /// ... + /// } + /// ``` + /// - Both Maven artifact data and folder paths ([GraphOutput#ARTIFACT_AND_FOLDER]), format + /// ```json + /// { + /// "{groupId}:{artifactId}": [ + /// { + /// "artifact": { + /// "groupId": "{groupId}", + /// "artifactId": "{artifactId}" + /// }, + /// "folder": "{folder}" + /// },... + /// ], + /// ... + /// } + /// ``` + /// + /// The value of this field is required and defaults to [GraphOutput#ARTIFACT_AND_FOLDER]. + /// It is set via the Maven plugin parameter `versioning.graphOutput`. + @Parameter(property = "versioning.graphOutput", required = true, defaultValue = "ARTIFACT_AND_FOLDER") + GraphOutput graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + + /// Specifies the output file for the generated dependency graph. + /// + /// If this value is set to `null`, + /// the dependency graph output will be printed to the console instead of being written to an external file. + /// + /// Uses the Maven property `versioning.outputFile` to allow configuration via Maven's usage of properties + /// or command-line arguments. + @Parameter(property = "versioning.outputFile") + Path outputFile = null; + /// Executes the internal logic for creating a dependency graph representation of Maven projects in the current scope. /// This method performs the following steps: /// 1. Resolves the execution root directory and logs it for debugging. @@ -72,7 +127,7 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep artifact -> collectProjectDependencies(artifact, dependencyToProjectArtifactMapping) )); - Map graph = projectArtifacts.keySet() + Map graph = projectArtifacts.keySet() .stream() .collect(Collectors.toMap( Function.identity(), @@ -82,9 +137,60 @@ protected void internalExecute() throws MojoExecutionException, MojoFailureExcep produceGraphOutput(graph); } - private void produceGraphOutput(Map graph) throws MojoExecutionException { - // TODO: Enhance with different outputs and switching between console and file output - System.out.println(Utils.writeObjectAsJson(graph)); + /// Produces the output of the dependency graph for Maven artifacts based on the specified graph output type. + /// The method transforms the given dependency graph into a format determined by the output configuration + /// (artifact-only, folder-only, or a combination of both) and serializes the resulting data as JSON. + /// The JSON output is either written to a file or printed to the console. + /// + /// @param graph a mapping of Maven artifacts to their corresponding detailed graph nodes, representing the dependency relationships between the artifacts + /// @throws MojoExecutionException if an error occurs while transforming the graph or writing the output to a file + private void produceGraphOutput(Map graph) throws MojoExecutionException { + Function mapper = Object::toString; + Object output = switch (graphOutput) { + case ARTIFACT_ONLY -> transformGraph(graph, mapper.compose(ArtifactLocation::artifact)); + case FOLDER_ONLY -> transformGraph(graph, ArtifactLocation::folder); + case ARTIFACT_AND_FOLDER -> transformGraph(graph, Function.identity()); + }; + + String objectAsJson = Utils.writeObjectAsJson(output); + if (outputFile != null) { + try (BufferedWriter writer = Files.newBufferedWriter( + outputFile, + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING + )) { + writer.write(objectAsJson); + } catch (IOException e) { + throw new MojoExecutionException("Unable to write to output file '%s'".formatted(outputFile), e); + } + } else { + System.out.println(objectAsJson); + } + } + + /// Transforms a dependency graph of Maven artifacts into a new mapping where the dependencies + /// of each artifact are processed using the provided mapping function. + /// + /// @param the target type of the transformation + /// @param graph a mapping of Maven artifacts to their detailed graph nodes, representing dependency relationships between artifacts + /// @param mapper a function that maps artifact locations (dependencies) to the desired target type + /// @return a transformed mapping of Maven artifacts to lists of processed dependencies where each dependency is mapped using the provided function + private Map> transformGraph( + Map graph, + Function mapper + ) { + return graph.entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue() + .dependencies() + .stream() + .map(mapper) + .toList() + )); } /// Resolves and collects all Maven artifacts that are dependent on the specified artifact within the provided @@ -168,34 +274,18 @@ private String getProjectFolderAsString(MavenProject project, Path executionRoot /// @param projectToDependenciesMapping a mapping of Maven projects to their respective dependencies; used to determine the dependencies of the provided artifact; must not be null /// @param projectArtifacts a mapping of Maven artifacts to their corresponding folder paths; must not be null /// @return a `Node` instance representing the given artifact, its folder path, and its resolved dependencies - private Node prepareMavenProjectNode( + private DetailedGraphNode prepareMavenProjectNode( MavenArtifact artifact, Map> projectToDependenciesMapping, Map projectArtifacts ) { - List minDependencies = projectToDependenciesMapping.getOrDefault(artifact, List.of()).stream() - .map(depArtifact -> new MinDependency(depArtifact, projectArtifacts.get(depArtifact))) + List minDependencies = projectToDependenciesMapping.getOrDefault(artifact, List.of()).stream() + .map(depArtifact -> new ArtifactLocation(depArtifact, projectArtifacts.get(depArtifact))) .toList(); - return new Node( + return new DetailedGraphNode( artifact, projectArtifacts.get(artifact), minDependencies ); } - - public record MinDependency(MavenArtifact artifact, String folder) { - public MinDependency { - Objects.requireNonNull(artifact, "`artifact` must not be null"); - Objects.requireNonNull(folder, "`folder` must not be null"); - } - } - - public record Node(MavenArtifact artifact, String folder, List dependencies) { - - public Node { - Objects.requireNonNull(artifact, "`artifact` must not be null"); - Objects.requireNonNull(folder, "`folder` must not be null"); - Objects.requireNonNull(dependencies, "`dependencies` must not be null"); - } - } } diff --git a/src/main/java/io/github/bsels/semantic/version/models/graph/ArtifactLocation.java b/src/main/java/io/github/bsels/semantic/version/models/graph/ArtifactLocation.java new file mode 100644 index 0000000..ed5f292 --- /dev/null +++ b/src/main/java/io/github/bsels/semantic/version/models/graph/ArtifactLocation.java @@ -0,0 +1,12 @@ +package io.github.bsels.semantic.version.models.graph; + +import io.github.bsels.semantic.version.models.MavenArtifact; + +import java.util.Objects; + +public record ArtifactLocation(MavenArtifact artifact, String folder) { + public ArtifactLocation { + Objects.requireNonNull(artifact, "`artifact` must not be null"); + Objects.requireNonNull(folder, "`folder` must not be null"); + } +} diff --git a/src/main/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNode.java b/src/main/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNode.java new file mode 100644 index 0000000..5ca1cff --- /dev/null +++ b/src/main/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNode.java @@ -0,0 +1,15 @@ +package io.github.bsels.semantic.version.models.graph; + +import io.github.bsels.semantic.version.models.MavenArtifact; + +import java.util.List; +import java.util.Objects; + +public record DetailedGraphNode(MavenArtifact artifact, String folder, List dependencies) { + + public DetailedGraphNode { + Objects.requireNonNull(artifact, "`artifact` must not be null"); + Objects.requireNonNull(folder, "`folder` must not be null"); + Objects.requireNonNull(dependencies, "`dependencies` must not be null"); + } +} diff --git a/src/main/java/io/github/bsels/semantic/version/models/graph/package-info.java b/src/main/java/io/github/bsels/semantic/version/models/graph/package-info.java new file mode 100644 index 0000000..5089c39 --- /dev/null +++ b/src/main/java/io/github/bsels/semantic/version/models/graph/package-info.java @@ -0,0 +1,5 @@ +/// Contains data models specifically for representing and managing the dependency graph of Maven projects. +/// +/// These models facilitate the visualization and processing of project dependencies, +/// including their artifact identifiers and physical folder locations. +package io.github.bsels.semantic.version.models.graph; \ No newline at end of file diff --git a/src/main/java/io/github/bsels/semantic/version/models/package-info.java b/src/main/java/io/github/bsels/semantic/version/models/package-info.java new file mode 100644 index 0000000..8c00102 --- /dev/null +++ b/src/main/java/io/github/bsels/semantic/version/models/package-info.java @@ -0,0 +1,5 @@ +/// Contains the core data models for the semantic version Maven plugin. +/// +/// These models represent semantic versions, version changes, changelog entries in Markdown format, Maven artifacts, +/// and supporting structures. +package io.github.bsels.semantic.version.models; \ No newline at end of file diff --git a/src/main/java/io/github/bsels/semantic/version/package-info.java b/src/main/java/io/github/bsels/semantic/version/package-info.java index d362848..2eda729 100644 --- a/src/main/java/io/github/bsels/semantic/version/package-info.java +++ b/src/main/java/io/github/bsels/semantic/version/package-info.java @@ -1,2 +1,5 @@ -/// This package contains all the Mojos of the semantic version Maven plugin +/// This package contains the Mojos (Maven plugin goals) of the semantic version Maven plugin. +/// +/// Mojos provide the core functionality such as updating POM versions, creating version Markdown, verifying versions, +/// and generating dependency graphs. package io.github.bsels.semantic.version; \ No newline at end of file diff --git a/src/main/java/io/github/bsels/semantic/version/parameters/GraphOutput.java b/src/main/java/io/github/bsels/semantic/version/parameters/GraphOutput.java new file mode 100644 index 0000000..0ae8f94 --- /dev/null +++ b/src/main/java/io/github/bsels/semantic/version/parameters/GraphOutput.java @@ -0,0 +1,14 @@ +package io.github.bsels.semantic.version.parameters; + +/// Specifies the format of the dependency graph output. +/// +/// This enum defines the available output formats for the `graph` Mojo. +/// The choice of output determines what information is included for each node in the dependency graph. +public enum GraphOutput { + /// Includes only the Maven artifact identifier (groupId:artifactId). + ARTIFACT_ONLY, + /// Includes only the folder path of the project. + FOLDER_ONLY, + /// Includes both the Maven artifact identifier and the project folder path. + ARTIFACT_AND_FOLDER +} diff --git a/src/main/java/io/github/bsels/semantic/version/parameters/package-info.java b/src/main/java/io/github/bsels/semantic/version/parameters/package-info.java index ee71e13..21c47f2 100644 --- a/src/main/java/io/github/bsels/semantic/version/parameters/package-info.java +++ b/src/main/java/io/github/bsels/semantic/version/parameters/package-info.java @@ -1,2 +1,5 @@ -/// This package contains the necessary classes for the plugin parameters +/// Contains parameter classes and enums used by the semantic version Maven plugin Mojos. +/// +/// These classes define configuration options such as version bump types, git settings, +/// verification modes, and artifact identifiers. package io.github.bsels.semantic.version.parameters; \ No newline at end of file diff --git a/src/main/java/io/github/bsels/semantic/version/utils/mapper/package-info.java b/src/main/java/io/github/bsels/semantic/version/utils/mapper/package-info.java index dc01e84..8523b50 100644 --- a/src/main/java/io/github/bsels/semantic/version/utils/mapper/package-info.java +++ b/src/main/java/io/github/bsels/semantic/version/utils/mapper/package-info.java @@ -1,2 +1,5 @@ -/// Custom serialization and deserialization utilities for the Semantic Versioning plugin. +/// Custom Jackson serializers and deserializers for the semantic version Maven plugin. +/// +/// Primarily handles specialized serialization of [io.github.bsels.semantic.version.models.MavenArtifact] +/// for JSON processing, supporting artifact-only keys and full representations. package io.github.bsels.semantic.version.utils.mapper; \ No newline at end of file diff --git a/src/main/java/io/github/bsels/semantic/version/utils/package-info.java b/src/main/java/io/github/bsels/semantic/version/utils/package-info.java index 0c356b0..160d920 100644 --- a/src/main/java/io/github/bsels/semantic/version/utils/package-info.java +++ b/src/main/java/io/github/bsels/semantic/version/utils/package-info.java @@ -1,2 +1,8 @@ -/// This package contains the utils class for processing POM files, Markdown files, Terminal interaction, and other utility functions. +/// Utility classes for various tasks in the semantic version Maven plugin. +/// +/// Includes processing of POM and Markdown files, terminal interactions, process execution, +/// and general helper functions. +/// +/// @see io.github.bsels.semantic.version.utils.mapper for serialization utils +/// @see io.github.bsels.semantic.version.utils.yaml.front.block for YAML front matter package io.github.bsels.semantic.version.utils; \ No newline at end of file diff --git a/src/main/java/io/github/bsels/semantic/version/utils/yaml/front/block/package-info.java b/src/main/java/io/github/bsels/semantic/version/utils/yaml/front/block/package-info.java index 10637ba..860476f 100644 --- a/src/main/java/io/github/bsels/semantic/version/utils/yaml/front/block/package-info.java +++ b/src/main/java/io/github/bsels/semantic/version/utils/yaml/front/block/package-info.java @@ -1,2 +1,5 @@ -/// This package contains the YAML Front Matter Block Parser and Renderer for the extension with CommonMark. +/// YAML Front Matter block parser and renderer for CommonMark Markdown extension. +/// +/// Enables parsing and rendering of YAML front matter blocks in Markdown documents, +/// used for changelog and version metadata extraction. package io.github.bsels.semantic.version.utils.yaml.front.block; \ No newline at end of file From 11c5cae19844db607352c16c8ce4d80a292b0e6e Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 16:02:18 +0100 Subject: [PATCH 12/21] Add `pom` to POM files and improve documentation for graph models --- .../version/models/graph/ArtifactLocation.java | 13 +++++++++++++ .../version/models/graph/DetailedGraphNode.java | 14 ++++++++++++++ .../bsels/semantic/version/UpdatePomMojoTest.java | 14 ++++++++++++++ .../resources/itests/leaves/intermediate/pom.xml | 2 ++ src/test/resources/itests/leaves/pom.xml | 2 ++ src/test/resources/itests/multi-recursive/pom.xml | 2 ++ src/test/resources/itests/revision/multi/pom.xml | 2 ++ 7 files changed, 49 insertions(+) diff --git a/src/main/java/io/github/bsels/semantic/version/models/graph/ArtifactLocation.java b/src/main/java/io/github/bsels/semantic/version/models/graph/ArtifactLocation.java index ed5f292..623c16f 100644 --- a/src/main/java/io/github/bsels/semantic/version/models/graph/ArtifactLocation.java +++ b/src/main/java/io/github/bsels/semantic/version/models/graph/ArtifactLocation.java @@ -4,7 +4,20 @@ import java.util.Objects; +/// Represents the physical location of a Maven artifact within the project structure. +/// +/// This record associates a [MavenArtifact] with its corresponding folder path, +/// which can be either absolute or relative depending on the plugin configuration. +/// +/// @param artifact the Maven artifact; must not be null +/// @param folder the folder path where the artifact's project is located; must not be null public record ArtifactLocation(MavenArtifact artifact, String folder) { + + /// Constructs a new `ArtifactLocation` and validates that its components are non-null. + /// + /// @param artifact the Maven artifact + /// @param folder the folder path + /// @throws NullPointerException if `artifact` or `folder` is null public ArtifactLocation { Objects.requireNonNull(artifact, "`artifact` must not be null"); Objects.requireNonNull(folder, "`folder` must not be null"); diff --git a/src/main/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNode.java b/src/main/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNode.java index 5ca1cff..df58b96 100644 --- a/src/main/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNode.java +++ b/src/main/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNode.java @@ -5,8 +5,22 @@ import java.util.List; import java.util.Objects; +/// Represents a detailed node in the dependency graph, including the artifact's location and its dependencies. +/// +/// This record contains comprehensive information about a project artifact, including its identity, +/// its physical location, and a list of its internal project dependencies with their locations. +/// +/// @param artifact the Maven artifact representing the project; must not be null +/// @param folder the folder path of the project; must not be null +/// @param dependencies the list of internal project dependencies and their locations; must not be null public record DetailedGraphNode(MavenArtifact artifact, String folder, List dependencies) { + /// Constructs a new `DetailedGraphNode` and validates that its components are non-null. + /// + /// @param artifact the Maven artifact + /// @param folder the folder path + /// @param dependencies the list of dependencies + /// @throws NullPointerException if `artifact`, `folder`, or `dependencies` is null public DetailedGraphNode { Objects.requireNonNull(artifact, "`artifact` must not be null"); Objects.requireNonNull(folder, "`folder` must not be null"); diff --git a/src/test/java/io/github/bsels/semantic/version/UpdatePomMojoTest.java b/src/test/java/io/github/bsels/semantic/version/UpdatePomMojoTest.java index 4382554..24b24c2 100644 --- a/src/test/java/io/github/bsels/semantic/version/UpdatePomMojoTest.java +++ b/src/test/java/io/github/bsels/semantic/version/UpdatePomMojoTest.java @@ -1285,6 +1285,8 @@ void handleMultiRecursiveProjectCorrect_NoErrors() { parent 6.1.0-parent + pom + child-1 child-2 @@ -1438,6 +1440,8 @@ void fixedVersionBump_Valid(VersionBump versionBump) { parent ${revision} + pom + %s @@ -1523,6 +1527,8 @@ void fixedVersionBumpWithBackup_Valid(VersionBump versionBump) { parent ${revision} + pom + %s @@ -1619,6 +1625,8 @@ void fixedVersionBumpDryRun_Valid(VersionBump versionBump) { parent ${revision} + pom + %s @@ -1875,6 +1883,8 @@ void singleSemanticVersionBumFile_Valid(String folder, String title, String expe parent ${revision} + pom + %s @@ -2000,6 +2010,8 @@ void multipleSemanticVersionBumpFiles_Valid() { parent ${revision} + pom + 4.0.0 @@ -2154,6 +2166,8 @@ void multipleSemanticVersionBumpFiles_CustomHeaders_Valid() { parent ${revision} + pom + 4.0.0 diff --git a/src/test/resources/itests/leaves/intermediate/pom.xml b/src/test/resources/itests/leaves/intermediate/pom.xml index c3fd102..ad5546e 100644 --- a/src/test/resources/itests/leaves/intermediate/pom.xml +++ b/src/test/resources/itests/leaves/intermediate/pom.xml @@ -7,6 +7,8 @@ intermediate 5.0.0-intermediate + pom + child-2 child-3 diff --git a/src/test/resources/itests/leaves/pom.xml b/src/test/resources/itests/leaves/pom.xml index faba402..4cfcd02 100644 --- a/src/test/resources/itests/leaves/pom.xml +++ b/src/test/resources/itests/leaves/pom.xml @@ -7,6 +7,8 @@ root 5.0.0-root + pom + child-1 intermediate diff --git a/src/test/resources/itests/multi-recursive/pom.xml b/src/test/resources/itests/multi-recursive/pom.xml index 092b9e4..a92808f 100644 --- a/src/test/resources/itests/multi-recursive/pom.xml +++ b/src/test/resources/itests/multi-recursive/pom.xml @@ -7,6 +7,8 @@ parent 6.0.0-parent + pom + child-1 child-2 diff --git a/src/test/resources/itests/revision/multi/pom.xml b/src/test/resources/itests/revision/multi/pom.xml index 472d1fe..7bee1a3 100644 --- a/src/test/resources/itests/revision/multi/pom.xml +++ b/src/test/resources/itests/revision/multi/pom.xml @@ -7,6 +7,8 @@ parent ${revision} + pom + 3.0.0 From b09dc11d10ee9b7d67cd32c8cfc2c3ee2d8cab5e Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 16:04:03 +0100 Subject: [PATCH 13/21] Improve `DependencyGraphMojo` documentation with detailed class and constructor-level comments --- .../semantic/version/DependencyGraphMojo.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java index 56de066..90896c9 100644 --- a/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java +++ b/src/main/java/io/github/bsels/semantic/version/DependencyGraphMojo.java @@ -29,6 +29,18 @@ import java.util.function.Function; import java.util.stream.Collectors; +/// Represents a Maven Mojo goal for generating a dependency graph of Maven projects within the current execution scope. +/// This goal facilitates the extraction, transformation, and representation of dependency relationships among Maven +/// project artifacts and supports producing the graph output in a configurable format. +/// +/// This Mojo is typically used during Maven builds to analyze project dependencies and produce insights +/// into the structure of the dependency tree, which may assist in understanding transitive dependencies, +/// resolving conflicts, or debugging build issues. +/// +/// The generated dependency graph can include both direct and transitive dependencies and is represented +/// as directed graph nodes, where each node corresponds to a Maven project artifact. +/// +/// The final graph representation, including its format and location, is configurable via parameters. @Mojo(name = "graph", aggregator = true, requiresDependencyResolution = ResolutionScope.NONE) @Execute(phase = LifecyclePhase.NONE) public final class DependencyGraphMojo extends BaseMojo { @@ -91,6 +103,15 @@ public final class DependencyGraphMojo extends BaseMojo { @Parameter(property = "versioning.outputFile") Path outputFile = null; + /// Default constructor for the `DependencyGraphMojo` class. + /// Initializes an instance by invoking the superclass constructor. + /// + /// This constructor is typically used by the Maven framework during the build process to instantiate + /// the goal implementation, enabling the execution of its logic. + public DependencyGraphMojo() { + super(); + } + /// Executes the internal logic for creating a dependency graph representation of Maven projects in the current scope. /// This method performs the following steps: /// 1. Resolves the execution root directory and logs it for debugging. From ecefa0ac95e91244d6b88a4db4dae56123dac3b8 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 16:09:51 +0100 Subject: [PATCH 14/21] Add unit tests for `ArtifactLocation`, `DetailedGraphNode`, and `GraphOutput` classes --- .../models/graph/ArtifactLocationTest.java | 96 ++++++++++++ .../models/graph/DetailedGraphNodeTest.java | 141 ++++++++++++++++++ .../version/parameters/GraphOutputTest.java | 33 ++++ 3 files changed, 270 insertions(+) create mode 100644 src/test/java/io/github/bsels/semantic/version/models/graph/ArtifactLocationTest.java create mode 100644 src/test/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNodeTest.java create mode 100644 src/test/java/io/github/bsels/semantic/version/parameters/GraphOutputTest.java diff --git a/src/test/java/io/github/bsels/semantic/version/models/graph/ArtifactLocationTest.java b/src/test/java/io/github/bsels/semantic/version/models/graph/ArtifactLocationTest.java new file mode 100644 index 0000000..af9ab7e --- /dev/null +++ b/src/test/java/io/github/bsels/semantic/version/models/graph/ArtifactLocationTest.java @@ -0,0 +1,96 @@ +package io.github.bsels.semantic.version.models.graph; + +import io.github.bsels.semantic.version.models.MavenArtifact; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ArtifactLocationTest { + private static final String GROUP_ID = "groupId"; + private static final String ARTIFACT_ID = "artifactId"; + private static final String FOLDER = "folder/path"; + + @Nested + class ConstructorTest { + + @ParameterizedTest + @CsvSource(value = { + "null,null,artifact", + "null," + FOLDER + ",artifact", + GROUP_ID + ":" + ARTIFACT_ID + ",null,folder" + }, nullValues = "null") + void nullInput_ThrowsNullPointerException(String artifactString, String folder, String exceptionParameter) { + MavenArtifact artifact = artifactString == null ? null : MavenArtifact.of(artifactString); + assertThatThrownBy(() -> new ArtifactLocation(artifact, folder)) + .isInstanceOf(NullPointerException.class) + .hasMessage("`%s` must not be null", exceptionParameter); + } + + @Test + void validInputs_ReturnsCorrectArtifactLocation() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + ArtifactLocation location = new ArtifactLocation(artifact, FOLDER); + assertThat(location) + .isNotNull() + .hasFieldOrPropertyWithValue("artifact", artifact) + .hasFieldOrPropertyWithValue("folder", FOLDER); + } + } + + @Nested + class AccessorTest { + + @Test + void artifact_ReturnsCorrectValue() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + ArtifactLocation location = new ArtifactLocation(artifact, FOLDER); + assertThat(location.artifact()) + .isEqualTo(artifact); + } + + @Test + void folder_ReturnsCorrectValue() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + ArtifactLocation location = new ArtifactLocation(artifact, FOLDER); + assertThat(location.folder()) + .isEqualTo(FOLDER); + } + } + + @Nested + class EqualsAndHashCodeTest { + + @Test + void sameValues_AreEqual() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + ArtifactLocation location1 = new ArtifactLocation(artifact, FOLDER); + ArtifactLocation location2 = new ArtifactLocation(artifact, FOLDER); + assertThat(location1) + .isEqualTo(location2) + .hasSameHashCodeAs(location2); + } + + @Test + void differentArtifact_AreNotEqual() { + MavenArtifact artifact1 = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + MavenArtifact artifact2 = new MavenArtifact("otherGroup", ARTIFACT_ID); + ArtifactLocation location1 = new ArtifactLocation(artifact1, FOLDER); + ArtifactLocation location2 = new ArtifactLocation(artifact2, FOLDER); + assertThat(location1) + .isNotEqualTo(location2); + } + + @Test + void differentFolder_AreNotEqual() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + ArtifactLocation location1 = new ArtifactLocation(artifact, FOLDER); + ArtifactLocation location2 = new ArtifactLocation(artifact, "other/folder"); + assertThat(location1) + .isNotEqualTo(location2); + } + } +} diff --git a/src/test/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNodeTest.java b/src/test/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNodeTest.java new file mode 100644 index 0000000..153a1c5 --- /dev/null +++ b/src/test/java/io/github/bsels/semantic/version/models/graph/DetailedGraphNodeTest.java @@ -0,0 +1,141 @@ +package io.github.bsels.semantic.version.models.graph; + +import io.github.bsels.semantic.version.models.MavenArtifact; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DetailedGraphNodeTest { + private static final String GROUP_ID = "groupId"; + private static final String ARTIFACT_ID = "artifactId"; + private static final String FOLDER = "folder/path"; + + @Nested + class ConstructorTest { + + @ParameterizedTest + @CsvSource(value = { + "null,null,null,artifact", + "null," + FOLDER + ",empty,artifact", + GROUP_ID + ":" + ARTIFACT_ID + ",null,empty,folder", + GROUP_ID + ":" + ARTIFACT_ID + "," + FOLDER + ",null,dependencies" + }, nullValues = "null") + void nullInput_ThrowsNullPointerException(String artifactString, String folder, String dependenciesType, String exceptionParameter) { + MavenArtifact artifact = artifactString == null ? null : MavenArtifact.of(artifactString); + List dependencies = dependenciesType == null ? null : List.of(); + assertThatThrownBy(() -> new DetailedGraphNode(artifact, folder, dependencies)) + .isInstanceOf(NullPointerException.class) + .hasMessage("`%s` must not be null", exceptionParameter); + } + + @Test + void validInputs_ReturnsCorrectDetailedGraphNode() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + List dependencies = List.of(); + DetailedGraphNode node = new DetailedGraphNode(artifact, FOLDER, dependencies); + assertThat(node) + .isNotNull() + .hasFieldOrPropertyWithValue("artifact", artifact) + .hasFieldOrPropertyWithValue("folder", FOLDER) + .hasFieldOrPropertyWithValue("dependencies", dependencies); + } + + @Test + void validInputsWithDependencies_ReturnsCorrectDetailedGraphNode() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + MavenArtifact depArtifact = new MavenArtifact("depGroup", "depArtifact"); + ArtifactLocation depLocation = new ArtifactLocation(depArtifact, "dep/folder"); + List dependencies = List.of(depLocation); + DetailedGraphNode node = new DetailedGraphNode(artifact, FOLDER, dependencies); + assertThat(node) + .isNotNull() + .hasFieldOrPropertyWithValue("artifact", artifact) + .hasFieldOrPropertyWithValue("folder", FOLDER) + .hasFieldOrPropertyWithValue("dependencies", dependencies); + } + } + + @Nested + class AccessorTest { + + @Test + void artifact_ReturnsCorrectValue() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + DetailedGraphNode node = new DetailedGraphNode(artifact, FOLDER, List.of()); + assertThat(node.artifact()) + .isEqualTo(artifact); + } + + @Test + void folder_ReturnsCorrectValue() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + DetailedGraphNode node = new DetailedGraphNode(artifact, FOLDER, List.of()); + assertThat(node.folder()) + .isEqualTo(FOLDER); + } + + @Test + void dependencies_ReturnsCorrectValue() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + MavenArtifact depArtifact = new MavenArtifact("depGroup", "depArtifact"); + ArtifactLocation depLocation = new ArtifactLocation(depArtifact, "dep/folder"); + List dependencies = List.of(depLocation); + DetailedGraphNode node = new DetailedGraphNode(artifact, FOLDER, dependencies); + assertThat(node.dependencies()) + .isEqualTo(dependencies) + .hasSize(1) + .containsExactly(depLocation); + } + } + + @Nested + class EqualsAndHashCodeTest { + + @Test + void sameValues_AreEqual() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + List dependencies = List.of(); + DetailedGraphNode node1 = new DetailedGraphNode(artifact, FOLDER, dependencies); + DetailedGraphNode node2 = new DetailedGraphNode(artifact, FOLDER, dependencies); + assertThat(node1) + .isEqualTo(node2) + .hasSameHashCodeAs(node2); + } + + @Test + void differentArtifact_AreNotEqual() { + MavenArtifact artifact1 = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + MavenArtifact artifact2 = new MavenArtifact("otherGroup", ARTIFACT_ID); + DetailedGraphNode node1 = new DetailedGraphNode(artifact1, FOLDER, List.of()); + DetailedGraphNode node2 = new DetailedGraphNode(artifact2, FOLDER, List.of()); + assertThat(node1) + .isNotEqualTo(node2); + } + + @Test + void differentFolder_AreNotEqual() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + DetailedGraphNode node1 = new DetailedGraphNode(artifact, FOLDER, List.of()); + DetailedGraphNode node2 = new DetailedGraphNode(artifact, "other/folder", List.of()); + assertThat(node1) + .isNotEqualTo(node2); + } + + @Test + void differentDependencies_AreNotEqual() { + MavenArtifact artifact = new MavenArtifact(GROUP_ID, ARTIFACT_ID); + MavenArtifact depArtifact = new MavenArtifact("depGroup", "depArtifact"); + ArtifactLocation depLocation = new ArtifactLocation(depArtifact, "dep/folder"); + DetailedGraphNode node1 = new DetailedGraphNode(artifact, FOLDER, List.of()); + DetailedGraphNode node2 = new DetailedGraphNode(artifact, FOLDER, List.of(depLocation)); + assertThat(node1) + .isNotEqualTo(node2); + } + } +} diff --git a/src/test/java/io/github/bsels/semantic/version/parameters/GraphOutputTest.java b/src/test/java/io/github/bsels/semantic/version/parameters/GraphOutputTest.java new file mode 100644 index 0000000..1eebf0a --- /dev/null +++ b/src/test/java/io/github/bsels/semantic/version/parameters/GraphOutputTest.java @@ -0,0 +1,33 @@ +package io.github.bsels.semantic.version.parameters; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GraphOutputTest { + + @Test + void numberOfEnumElements_Return3() { + assertThat(GraphOutput.values()) + .hasSize(3) + .extracting(GraphOutput::name) + .containsExactlyInAnyOrder("ARTIFACT_ONLY", "FOLDER_ONLY", "ARTIFACT_AND_FOLDER"); + } + + @ParameterizedTest + @EnumSource(GraphOutput.class) + void toString_ReturnsCorrectValue(GraphOutput graphOutput) { + assertThat(graphOutput.toString()) + .isEqualTo(graphOutput.name()); + + } + + @ParameterizedTest + @EnumSource(GraphOutput.class) + void valueOf_ReturnCorrectValue(GraphOutput graphOutput) { + assertThat(GraphOutput.valueOf(graphOutput.toString())) + .isEqualTo(graphOutput); + } +} From 1776fca0e8ba8f0f45dde57f97c99e048d69a73f Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 16:30:17 +0100 Subject: [PATCH 15/21] Add extensive unit tests for `DependencyGraphMojo` to validate single, multi-project, and edge case scenarios across all graph output formats with mocked file handling and exception testing --- .../version/DependencyGraphMojoTest.java | 743 +++++++++++++++++- 1 file changed, 726 insertions(+), 17 deletions(-) diff --git a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java index 71676aa..4264e02 100644 --- a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java +++ b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java @@ -1,46 +1,755 @@ package io.github.bsels.semantic.version; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import io.github.bsels.semantic.version.models.MavenArtifact; +import io.github.bsels.semantic.version.models.graph.ArtifactLocation; +import io.github.bsels.semantic.version.parameters.GraphOutput; import io.github.bsels.semantic.version.test.utils.ReadMockedMavenSession; import io.github.bsels.semantic.version.test.utils.TestLog; -import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Spy; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.OpenOption; import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.verify; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class DependencyGraphMojoTest extends AbstractBaseMojoTest { - @Spy private DependencyGraphMojo classUnderTest; + private TestLog testLog; + private Map mockedOutputFiles; + private MockedStatic filesMockedStatic; + private ByteArrayOutputStream outputStream; + private PrintStream originalSystemOut; + private ObjectMapper objectMapper; @BeforeEach void setUp() { - classUnderTest.setLog(new TestLog(TestLog.LogLevel.NONE)); + classUnderTest = new DependencyGraphMojo(); + testLog = new TestLog(TestLog.LogLevel.NONE); + classUnderTest.setLog(testLog); + mockedOutputFiles = new HashMap<>(); + objectMapper = new ObjectMapper(); + + filesMockedStatic = Mockito.mockStatic(Files.class, Mockito.CALLS_REAL_METHODS); + filesMockedStatic.when(() -> Files.newBufferedWriter(Mockito.any(), Mockito.any(), Mockito.any(OpenOption[].class))) + .thenAnswer(answer -> { + Path path = answer.getArgument(0); + mockedOutputFiles.put(path, new StringWriter()); + return new BufferedWriter(mockedOutputFiles.get(path)); + }); + + outputStream = new ByteArrayOutputStream(); + originalSystemOut = System.out; + System.setOut(new PrintStream(outputStream)); + } + + @AfterEach + void tearDown() { + System.setOut(originalSystemOut); + filesMockedStatic.close(); + } + + @Nested + class SingleProjectTests { + + @Test + void internalExecute_singleProject_artifactAndFolder_relativePaths_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("single"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(1); + MavenArtifact singleArtifact = new MavenArtifact("org.example.itests.single", "project"); + assertThat(graph).containsKey(singleArtifact); + assertThat(graph.get(singleArtifact)).hasSize(1); + assertThat(mockedOutputFiles).isEmpty(); + } + + @Test + void internalExecute_singleProject_artifactOnly_relativePaths_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("single"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_ONLY; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(1); + MavenArtifact singleArtifact = new MavenArtifact("org.example.itests.single", "project"); + assertThat(graph).containsKey(singleArtifact); + assertThat(graph.get(singleArtifact)).hasSize(1); + assertThat(mockedOutputFiles).isEmpty(); + } + + @Test + void internalExecute_singleProject_folderOnly_relativePaths_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("single"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.FOLDER_ONLY; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(1); + MavenArtifact singleArtifact = new MavenArtifact("org.example.itests.single", "project"); + assertThat(graph).containsKey(singleArtifact); + assertThat(graph.get(singleArtifact)).hasSize(1); + assertThat(mockedOutputFiles).isEmpty(); + } + + @Test + void internalExecute_singleProject_absolutePaths_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("single"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = false; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(1); + MavenArtifact singleArtifact = new MavenArtifact("org.example.itests.single", "project"); + assertThat(graph).containsKey(singleArtifact); + assertThat(graph.get(singleArtifact)).hasSize(1); + assertThat(mockedOutputFiles).isEmpty(); + } + + @Test + void internalExecute_singleProject_writeToFile() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("single"); + Path outputFile = Path.of("/tmp/graph-output.json"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = outputFile; + + // Act + classUnderTest.internalExecute(); + + // Assert + assertThat(mockedOutputFiles).containsKey(outputFile); + String fileContent = mockedOutputFiles.get(outputFile).toString(); + assertThat(fileContent).isNotEmpty(); + + Map> graph = objectMapper.readValue( + fileContent, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(1); + MavenArtifact singleArtifact = new MavenArtifact("org.example.itests.single", "project"); + assertThat(graph).containsKey(singleArtifact); + assertThat(outputStream.toString()).isEmpty(); + } + } + + @Nested + class MultiProjectTests { + + @Test + void internalExecute_multiProject_artifactAndFolder_relativePaths_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("multi"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + // Verify all projects are present + assertThat(graph).containsKeys( + new MavenArtifact("org.example.itests.multi", "parent"), + new MavenArtifact("org.example.itests.multi", "dependency"), + new MavenArtifact("org.example.itests.multi", "plugin"), + new MavenArtifact("org.example.itests.multi", "plugin-management"), + new MavenArtifact("org.example.itests.multi", "dependency-management"), + new MavenArtifact("org.example.itests.multi", "combination"), + new MavenArtifact("org.example.itests.multi", "excluded") + ); + + // Verify transitive dependencies for combination + MavenArtifact combination = new MavenArtifact("org.example.itests.multi", "combination"); + List combinationDeps = graph.get(combination); + assertThat(combinationDeps).hasSize(6); + + // Verify dependencies are in build order (topological sort) + List depArtifacts = combinationDeps.stream() + .map(ArtifactLocation::artifact) + .toList(); + + assertThat(depArtifacts).containsExactlyInAnyOrder( + new MavenArtifact("org.example.itests.multi", "parent"), + new MavenArtifact("org.example.itests.multi", "dependency-management"), + new MavenArtifact("org.example.itests.multi", "plugin-management"), + new MavenArtifact("org.example.itests.multi", "plugin"), + new MavenArtifact("org.example.itests.multi", "dependency"), + new MavenArtifact("org.example.itests.multi", "combination") + ); + + assertThat(mockedOutputFiles).isEmpty(); + } + + @Test + void internalExecute_multiProject_artifactOnly_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("multi"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_ONLY; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(7); + + // Verify combination has correct dependencies as strings + MavenArtifact combination = new MavenArtifact("org.example.itests.multi", "combination"); + List combinationDeps = graph.get(combination); + assertThat(combinationDeps).containsExactlyInAnyOrder( + "org.example.itests.multi:parent", + "org.example.itests.multi:dependency-management", + "org.example.itests.multi:plugin-management", + "org.example.itests.multi:plugin", + "org.example.itests.multi:dependency", + "org.example.itests.multi:combination" + ); + } + + @Test + void internalExecute_multiProject_folderOnly_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("multi"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.FOLDER_ONLY; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(7); + + // Verify combination has correct folder paths + MavenArtifact combination = new MavenArtifact("org.example.itests.multi", "combination"); + List combinationFolders = graph.get(combination); + assertThat(combinationFolders).hasSize(6); + assertThat(combinationFolders).allMatch(folder -> !folder.startsWith("/")); + } + + @Test + void internalExecute_multiProject_absolutePaths_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("multi"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = false; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(7); + + // Verify paths are absolute + MavenArtifact combination = new MavenArtifact("org.example.itests.multi", "combination"); + List combinationDeps = graph.get(combination); + assertThat(combinationDeps).hasSize(6); + assertThat(combinationDeps).allMatch(dep -> dep.folder().startsWith("/")); + } + + @Test + void internalExecute_multiProject_writeToFile() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("multi"); + Path outputFile = Path.of("/tmp/multi-graph.json"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = outputFile; + + // Act + classUnderTest.internalExecute(); + + // Assert + assertThat(mockedOutputFiles).containsKey(outputFile); + String fileContent = mockedOutputFiles.get(outputFile).toString(); + assertThat(fileContent).isNotEmpty(); + + Map> graph = objectMapper.readValue( + fileContent, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(7); + assertThat(outputStream.toString()).isEmpty(); + } + } + + @Nested + class ChainedDependencyTests { + + @Test + void internalExecute_chainedDependency_artifactAndFolder_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("chained-dependency"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + // Verify all projects are present + assertThat(graph).containsKeys( + new MavenArtifact("org.example.itests.chained", "root"), + new MavenArtifact("org.example.itests.chained", "a"), + new MavenArtifact("org.example.itests.chained", "b"), + new MavenArtifact("org.example.itests.chained", "c"), + new MavenArtifact("org.example.itests.chained", "d"), + new MavenArtifact("org.example.itests.chained", "e") + ); + + // Verify chained dependencies: e -> d -> c -> b -> a + MavenArtifact e = new MavenArtifact("org.example.itests.chained", "e"); + List eDeps = graph.get(e); + assertThat(eDeps).hasSize(5); + assertThat(eDeps.stream().map(ArtifactLocation::artifact)).containsExactlyInAnyOrder( + new MavenArtifact("org.example.itests.chained", "a"), + new MavenArtifact("org.example.itests.chained", "b"), + new MavenArtifact("org.example.itests.chained", "c"), + new MavenArtifact("org.example.itests.chained", "d"), + new MavenArtifact("org.example.itests.chained", "e") + ); + } + + @ParameterizedTest + @EnumSource(GraphOutput.class) + void internalExecute_chainedDependency_allGraphOutputs(GraphOutput graphOutput) throws Exception { + // Arrange + Path projectRoot = getResourcesPath("chained-dependency"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = graphOutput; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act & Assert + assertThatNoException().isThrownBy(() -> classUnderTest.internalExecute()); + + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + } + } + + @Nested + class LeavesProjectTests { + + @Test + void internalExecute_leavesProject_artifactAndFolder_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("leaves"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + // Verify all projects are present + assertThat(graph).containsKeys( + new MavenArtifact("org.example.itests.leaves", "root"), + new MavenArtifact("org.example.itests.leaves", "child-1"), + new MavenArtifact("org.example.itests.leaves", "intermediate"), + new MavenArtifact("org.example.itests.leaves", "child-2"), + new MavenArtifact("org.example.itests.leaves", "child-3") + ); + } + } + + @Nested + class MultiRecursiveProjectTests { + + @Test + void internalExecute_multiRecursiveProject_artifactAndFolder_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("multi-recursive"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + // Verify all projects are present + assertThat(graph).containsKeys( + new MavenArtifact("org.example.itests.multi-recursive", "parent"), + new MavenArtifact("org.example.itests.multi-recursive", "child-1"), + new MavenArtifact("org.example.itests.multi-recursive", "child-2") + ); + } + } + + @Nested + class RevisionProjectTests { + + @Test + void internalExecute_revisionMultiProject_artifactAndFolder_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("revision", "multi"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + // Verify all projects are present + assertThat(graph).containsKeys( + new MavenArtifact("org.example.itests.revision.multi", "parent"), + new MavenArtifact("org.example.itests.revision.multi", "child1"), + new MavenArtifact("org.example.itests.revision.multi", "child2") + ); + } + + @Test + void internalExecute_revisionSingleProject_artifactAndFolder_console() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("revision", "single"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + assertThat(output).isNotEmpty(); + + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + assertThat(graph).hasSize(1); + assertThat(graph).containsKey( + new MavenArtifact("org.example.itests.revision.single", "project") + ); + } } - @Test - void internalExecute_calculatesCorrectDependencies() throws MojoExecutionException, MojoFailureException { - // Arrange - Path projectRoot = getResourcesPath("multi"); - classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + @Nested + class ExceptionTests { + + @Test + void internalExecute_ioExceptionWhenWritingFile_throwsMojoExecutionException() { + // Arrange + Path projectRoot = getResourcesPath("single"); + Path outputFile = Path.of("/tmp/failing-output.json"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = outputFile; + + // Mock IOException when writing to file + filesMockedStatic.when(() -> Files.newBufferedWriter(Mockito.eq(outputFile), Mockito.any(), Mockito.any(OpenOption[].class))) + .thenThrow(new IOException("Simulated IO error")); + + // Act & Assert + assertThatExceptionOfType(MojoExecutionException.class) + .isThrownBy(() -> classUnderTest.internalExecute()) + .withMessageContaining("Unable to write to output file") + .withMessageContaining("/tmp/failing-output.json") + .withCauseInstanceOf(IOException.class); + } + + @Test + void internalExecute_ioExceptionDuringWrite_throwsMojoExecutionException() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("single"); + Path outputFile = Path.of("/tmp/write-error.json"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = outputFile; + + // Create a BufferedWriter that throws IOException on write + BufferedWriter failingWriter = Mockito.mock(BufferedWriter.class); + Mockito.doThrow(new IOException("Write failed")).when(failingWriter).write(Mockito.anyString()); + + filesMockedStatic.when(() -> Files.newBufferedWriter(Mockito.eq(outputFile), Mockito.any(), Mockito.any(OpenOption[].class))) + .thenReturn(failingWriter); + + // Act & Assert + assertThatExceptionOfType(MojoExecutionException.class) + .isThrownBy(() -> classUnderTest.internalExecute()) + .withMessageContaining("Unable to write to output file") + .withCauseInstanceOf(IOException.class); + } + } + + @Nested + class EdgeCaseTests { + + @Test + void internalExecute_rootProjectPath_usesCurrentDirectory() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("single"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + // Root project should have "." as folder when using relative paths + assertThat(graph).hasSize(1); + } + + @Test + void internalExecute_emptyDependencies_producesValidGraph() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("single"); + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + + // Act + classUnderTest.internalExecute(); + + // Assert + String output = outputStream.toString().trim(); + Map> graph = objectMapper.readValue( + output, + new TypeReference<>() { + } + ); + + MavenArtifact singleArtifact = new MavenArtifact("org.example.itests.single", "project"); + assertThat(graph.get(singleArtifact)).hasSize(1); + } + + @Test + void internalExecute_multipleGraphOutputFormats_produceConsistentResults() throws Exception { + // Arrange + Path projectRoot = getResourcesPath("multi"); + + // Test ARTIFACT_AND_FOLDER + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_AND_FOLDER; + classUnderTest.useRelativePaths = true; + classUnderTest.outputFile = null; + classUnderTest.internalExecute(); + String outputFull = outputStream.toString().trim(); + outputStream.reset(); + + // Test ARTIFACT_ONLY + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.ARTIFACT_ONLY; + classUnderTest.internalExecute(); + String outputArtifact = outputStream.toString().trim(); + outputStream.reset(); + + // Test FOLDER_ONLY + classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); + classUnderTest.graphOutput = GraphOutput.FOLDER_ONLY; + classUnderTest.internalExecute(); + String outputFolder = outputStream.toString().trim(); + + // Assert all produce valid JSON + assertThat(outputFull).isNotEmpty(); + assertThat(outputArtifact).isNotEmpty(); + assertThat(outputFolder).isNotEmpty(); - // Act - classUnderTest.internalExecute(); + // Verify they all have the same number of keys + Map graphFull = objectMapper.readValue(outputFull, new TypeReference<>() { + }); + Map graphArtifact = objectMapper.readValue(outputArtifact, new TypeReference<>() { + }); + Map graphFolder = objectMapper.readValue(outputFolder, new TypeReference<>() { + }); - // Assert - verify(classUnderTest).internalExecute(); + assertThat(graphFull).hasSameSizeAs(graphArtifact); + assertThat(graphFull).hasSameSizeAs(graphFolder); + } } } From 5b04697c4af0710a0c7415cbf23c1a59eeb65331 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 16:39:53 +0100 Subject: [PATCH 16/21] Add unit test for `MavenArtifactArtifactOnlySerializer` to validate artifact ID serialization --- .../MavenArtifactArtifactOnlySerializerTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/io/github/bsels/semantic/version/utils/mapper/MavenArtifactArtifactOnlySerializerTest.java b/src/test/java/io/github/bsels/semantic/version/utils/mapper/MavenArtifactArtifactOnlySerializerTest.java index 2f2c95f..cabadd0 100644 --- a/src/test/java/io/github/bsels/semantic/version/utils/mapper/MavenArtifactArtifactOnlySerializerTest.java +++ b/src/test/java/io/github/bsels/semantic/version/utils/mapper/MavenArtifactArtifactOnlySerializerTest.java @@ -38,4 +38,17 @@ void serializeKey_WritesArtifactIdFieldName() throws Exception { assertThat(json) .isEqualTo("{\"" + ARTIFACT_ID + "\":\"value\"}"); } + + @Test + void serializeRoot_WritesArtifactIdString() throws Exception { + ObjectMapper mapper = new ObjectMapper() + .registerModule(new SimpleModule() + .addSerializer(MavenArtifact.class, new MavenArtifactArtifactOnlySerializer()) + ); + + String json = mapper.writeValueAsString(new MavenArtifact(GROUP_ID, ARTIFACT_ID)); + + assertThat(json) + .isEqualTo("\"" + ARTIFACT_ID + "\""); + } } From 88e3971e1c2f7e6574839605c6d8ba70f3f1207c Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 16:54:07 +0100 Subject: [PATCH 17/21] Refactor test method names in `DependencyGraphMojoTest` to align with CamelCase naming conventions. --- .../version/DependencyGraphMojoTest.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java index 4264e02..66b30dd 100644 --- a/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java +++ b/src/test/java/io/github/bsels/semantic/version/DependencyGraphMojoTest.java @@ -77,7 +77,7 @@ void tearDown() { class SingleProjectTests { @Test - void internalExecute_singleProject_artifactAndFolder_relativePaths_console() throws Exception { + void internalExecute_SingleProject_ArtifactAndFolder_RelativePaths_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("single"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -106,7 +106,7 @@ void internalExecute_singleProject_artifactAndFolder_relativePaths_console() thr } @Test - void internalExecute_singleProject_artifactOnly_relativePaths_console() throws Exception { + void internalExecute_SingleProject_ArtifactOnly_RelativePaths_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("single"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -135,7 +135,7 @@ void internalExecute_singleProject_artifactOnly_relativePaths_console() throws E } @Test - void internalExecute_singleProject_folderOnly_relativePaths_console() throws Exception { + void internalExecute_SingleProject_FolderOnly_RelativePaths_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("single"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -164,7 +164,7 @@ void internalExecute_singleProject_folderOnly_relativePaths_console() throws Exc } @Test - void internalExecute_singleProject_absolutePaths_console() throws Exception { + void internalExecute_SingleProject_AbsolutePaths_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("single"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -193,7 +193,7 @@ void internalExecute_singleProject_absolutePaths_console() throws Exception { } @Test - void internalExecute_singleProject_writeToFile() throws Exception { + void internalExecute_SingleProject_WriteToFile() throws Exception { // Arrange Path projectRoot = getResourcesPath("single"); Path outputFile = Path.of("/tmp/graph-output.json"); @@ -227,7 +227,7 @@ void internalExecute_singleProject_writeToFile() throws Exception { class MultiProjectTests { @Test - void internalExecute_multiProject_artifactAndFolder_relativePaths_console() throws Exception { + void internalExecute_MultiProject_ArtifactAndFolder_RelativePaths_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("multi"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -282,7 +282,7 @@ void internalExecute_multiProject_artifactAndFolder_relativePaths_console() thro } @Test - void internalExecute_multiProject_artifactOnly_console() throws Exception { + void internalExecute_MultiProject_ArtifactOnly_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("multi"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -319,7 +319,7 @@ void internalExecute_multiProject_artifactOnly_console() throws Exception { } @Test - void internalExecute_multiProject_folderOnly_console() throws Exception { + void internalExecute_MultiProject_FolderOnly_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("multi"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -350,7 +350,7 @@ void internalExecute_multiProject_folderOnly_console() throws Exception { } @Test - void internalExecute_multiProject_absolutePaths_console() throws Exception { + void internalExecute_MultiProject_AbsolutePaths_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("multi"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -381,7 +381,7 @@ void internalExecute_multiProject_absolutePaths_console() throws Exception { } @Test - void internalExecute_multiProject_writeToFile() throws Exception { + void internalExecute_MultiProject_WriteToFile() throws Exception { // Arrange Path projectRoot = getResourcesPath("multi"); Path outputFile = Path.of("/tmp/multi-graph.json"); @@ -413,7 +413,7 @@ void internalExecute_multiProject_writeToFile() throws Exception { class ChainedDependencyTests { @Test - void internalExecute_chainedDependency_artifactAndFolder_console() throws Exception { + void internalExecute_ChainedDependency_ArtifactAndFolder_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("chained-dependency"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -459,7 +459,7 @@ void internalExecute_chainedDependency_artifactAndFolder_console() throws Except @ParameterizedTest @EnumSource(GraphOutput.class) - void internalExecute_chainedDependency_allGraphOutputs(GraphOutput graphOutput) throws Exception { + void internalExecute_ChainedDependency_AllGraphOutputs(GraphOutput graphOutput) throws Exception { // Arrange Path projectRoot = getResourcesPath("chained-dependency"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -479,7 +479,7 @@ void internalExecute_chainedDependency_allGraphOutputs(GraphOutput graphOutput) class LeavesProjectTests { @Test - void internalExecute_leavesProject_artifactAndFolder_console() throws Exception { + void internalExecute_LeavesProject_ArtifactAndFolder_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("leaves"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -515,7 +515,7 @@ void internalExecute_leavesProject_artifactAndFolder_console() throws Exception class MultiRecursiveProjectTests { @Test - void internalExecute_multiRecursiveProject_artifactAndFolder_console() throws Exception { + void internalExecute_MultiRecursiveProject_ArtifactAndFolder_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("multi-recursive"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -549,7 +549,7 @@ void internalExecute_multiRecursiveProject_artifactAndFolder_console() throws Ex class RevisionProjectTests { @Test - void internalExecute_revisionMultiProject_artifactAndFolder_console() throws Exception { + void internalExecute_RevisionMultiProject_ArtifactAndFolder_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("revision", "multi"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -579,7 +579,7 @@ void internalExecute_revisionMultiProject_artifactAndFolder_console() throws Exc } @Test - void internalExecute_revisionSingleProject_artifactAndFolder_console() throws Exception { + void internalExecute_RevisionSingleProject_ArtifactAndFolder_Console() throws Exception { // Arrange Path projectRoot = getResourcesPath("revision", "single"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -611,7 +611,7 @@ void internalExecute_revisionSingleProject_artifactAndFolder_console() throws Ex class ExceptionTests { @Test - void internalExecute_ioExceptionWhenWritingFile_throwsMojoExecutionException() { + void internalExecute_IoExceptionWhenWritingFile_ThrowsMojoExecutionException() { // Arrange Path projectRoot = getResourcesPath("single"); Path outputFile = Path.of("/tmp/failing-output.json"); @@ -633,7 +633,7 @@ void internalExecute_ioExceptionWhenWritingFile_throwsMojoExecutionException() { } @Test - void internalExecute_ioExceptionDuringWrite_throwsMojoExecutionException() throws Exception { + void internalExecute_IoExceptionDuringWrite_ThrowsMojoExecutionException() throws Exception { // Arrange Path projectRoot = getResourcesPath("single"); Path outputFile = Path.of("/tmp/write-error.json"); @@ -661,7 +661,7 @@ void internalExecute_ioExceptionDuringWrite_throwsMojoExecutionException() throw class EdgeCaseTests { @Test - void internalExecute_rootProjectPath_usesCurrentDirectory() throws Exception { + void internalExecute_RootProjectPath_UsesCurrentDirectory() throws Exception { // Arrange Path projectRoot = getResourcesPath("single"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -685,7 +685,7 @@ void internalExecute_rootProjectPath_usesCurrentDirectory() throws Exception { } @Test - void internalExecute_emptyDependencies_producesValidGraph() throws Exception { + void internalExecute_EmptyDependencies_ProducesValidGraph() throws Exception { // Arrange Path projectRoot = getResourcesPath("single"); classUnderTest.session = ReadMockedMavenSession.readMockedMavenSession(projectRoot, Path.of(".")); @@ -709,7 +709,7 @@ void internalExecute_emptyDependencies_producesValidGraph() throws Exception { } @Test - void internalExecute_multipleGraphOutputFormats_produceConsistentResults() throws Exception { + void internalExecute_MultipleGraphOutputFormats_ProduceConsistentResults() throws Exception { // Arrange Path projectRoot = getResourcesPath("multi"); From f07a90c1853e8bb44502a112acb7ccdf02d0c000 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 16:59:01 +0100 Subject: [PATCH 18/21] Update `README.md` to document the new `graph` goal with usage examples, configuration properties, output formats, and JSON schema definitions. --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/README.md b/README.md index 2dbad02..9cf3c11 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ A Maven plugin for automated semantic versioning with Markdown-based changelog m - [create](#create) - [update](#update) - [verify](#verify) + - [graph](#graph) - [Configuration Properties](#configuration-properties) - [Examples](#examples) - [License](#license) @@ -246,6 +247,94 @@ mvn io.github.bsels:semantic-version-maven-plugin:verify \ -Dversioning.verification.consistent=true ``` +--- + +### graph + +**Full name**: `io.github.bsels:semantic-version-maven-plugin:graph` + +**Description**: Generates a JSON representation of the dependency graph for Maven projects within the current execution +scope. It identifies internal dependencies between projects and can output the graph either to the console or to a +specified file. The output includes transitive dependencies sorted in build order (topological order). + +**Phase**: Not bound to any lifecycle phase (standalone goal) + +#### Configuration Properties + +| Property | Type | Default | Description | +|----------------------------|---------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `versioning.graphOutput` | `GraphOutput` | `ARTIFACT_AND_FOLDER` | Specifies the graph output format:
• `ARTIFACT_ONLY`: Only Maven artifact identifiers
• `FOLDER_ONLY`: Only project folder paths
• `ARTIFACT_AND_FOLDER`: Both artifact identifiers and folder paths | +| `versioning.outputFile` | `Path` | `-` | Path to the output file. If not specified, the graph is printed to the console. | +| `versioning.relativePaths` | `boolean` | `true` | When `true`, project folder paths are resolved relative to the execution root; otherwise, absolute paths are used. | +| `versioning.modus` | `Modus` | `PROJECT_VERSION` | Project scope for the graph:
• `PROJECT_VERSION`: All projects in multi-module builds
• `REVISION_PROPERTY`: Only current project using the `revision` property
• `PROJECT_VERSION_ONLY_LEAFS`: Only leaf projects (no modules) | + +#### Output Format Description + +The generated JSON is a map where each key is a project's artifact identifier (`groupId:artifactId`), and the value is a +list of its internal dependencies. + +**ARTIFACT_ONLY**: + +```json +{ + "io.github.bsels:parent": [], + "io.github.bsels:child": [ + "io.github.bsels:parent" + ] +} +``` + +**FOLDER_ONLY**: + +```json +{ + "io.github.bsels:parent": [], + "io.github.bsels:child": [ + "." + ] +} +``` + +**ARTIFACT_AND_FOLDER**: + +```json +{ + "io.github.bsels:parent": [], + "io.github.bsels:child": [ + { + "artifact": { + "groupId": "io.github.bsels", + "artifactId": "parent" + }, + "folder": "." + } + ] +} +``` + +#### Example Usage + +**Basic usage** (print to console): + +```bash +mvn io.github.bsels:semantic-version-maven-plugin:graph +``` + +**Write to file with absolute paths**: + +```bash +mvn io.github.bsels:semantic-version-maven-plugin:graph \ + -Dversioning.outputFile=graph.json \ + -Dversioning.relativePaths=false +``` + +**Artifact-only output**: + +```bash +mvn io.github.bsels:semantic-version-maven-plugin:graph \ + -Dversioning.graphOutput=ARTIFACT_ONLY +``` + ## Configuration Properties ### Common Properties @@ -290,6 +379,14 @@ These properties apply to `create`, `update`, and `verify` goals. The `verify` g | `versioning.verification.mode` | `VerificationMode` | `ALL_PROJECTS` | Verification scope:
• `NONE`: skip verification
• `AT_LEAST_ONE_PROJECT`: require at least one version-marked project
• `DEPENDENT_PROJECTS`: require all dependent projects to be version-marked
• `ALL_PROJECTS`: all projects in scope must be version-marked | | `versioning.verification.consistent` | `boolean` | `false` | When `true`, all version-marked projects must share the same version bump type | +### graph-Specific Properties + +| Property | Type | Default | Description | +|----------------------------|---------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `versioning.graphOutput` | `GraphOutput` | `ARTIFACT_AND_FOLDER` | Specifies the graph output format:
• `ARTIFACT_ONLY`: Only artifact identifiers
• `FOLDER_ONLY`: Only folder paths
• `ARTIFACT_AND_FOLDER`: Artifacts and folder paths | +| `versioning.outputFile` | `Path` | `-` | Path to the output file. If not specified, output is printed to the console. | +| `versioning.relativePaths` | `boolean` | `true` | When `true`, project paths are relative to execution root; otherwise, absolute. | + ## Examples ### Example 1: Single Project Workflow From 3b68fac9a7388ddf90bce4b987a21468c964c36d Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 17:01:09 +0100 Subject: [PATCH 19/21] Created version Markdown file for 1 project(s) --- .versioning/versioning-20260214170109.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .versioning/versioning-20260214170109.md diff --git a/.versioning/versioning-20260214170109.md b/.versioning/versioning-20260214170109.md new file mode 100644 index 0000000..c404b68 --- /dev/null +++ b/.versioning/versioning-20260214170109.md @@ -0,0 +1,5 @@ +--- +io.github.bsels:semantic-version-maven-plugin: "MINOR" +--- + +Added new `graph` goal, to list the different project and their internal dependencies. From 6e87068e0c12f57bd1ec702f4fef6907595901eb Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 17:07:18 +0100 Subject: [PATCH 20/21] Update project to release version 1.2.0 and upgrade plugin and dependency versions --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index e85133e..2449fea 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.github.bsels semantic-version-maven-plugin - 1.2.0-SNAPSHOT + 1.2.0 maven-plugin ${project.groupId}:${project.artifactId} @@ -43,8 +43,8 @@ 0.8.14 3.9.12 3.6.2 - 3.9.0 - 3.14.1 + 3.10.0 + 3.15.0 3.5.4 3.15.2 3.2.8 @@ -53,7 +53,7 @@ 0.10.0 - 3.27.6 + 3.27.7 0.27.1 2.21.0 6.0.2 From b1e3e341a65760fe9443375b864ff6a5f7024116 Mon Sep 17 00:00:00 2001 From: Boris Sels Date: Sat, 14 Feb 2026 17:09:33 +0100 Subject: [PATCH 21/21] Create versioning file and update dependencies: upgrade `maven-dependency-plugin` to 3.10.0, `maven-compiler-plugin` to 3.15.0, and `assertj-core` to 3.27.7 --- .versioning/versioning-20260214170800.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .versioning/versioning-20260214170800.md diff --git a/.versioning/versioning-20260214170800.md b/.versioning/versioning-20260214170800.md new file mode 100644 index 0000000..cf8edbd --- /dev/null +++ b/.versioning/versioning-20260214170800.md @@ -0,0 +1,8 @@ +--- +io.github.bsels:semantic-version-maven-plugin: "PATCH" +--- + +Updated the following project dependencies: +- `maven-dependency-plugin` from `3.9.0` to `3.10.0` +- `maven-compiler-plugin` from `3.14.1` to `3.15.0` +- `assertj-core` from `3.27.6` to `3.27.7`