diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/ConsoleIcon.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/ConsoleIcon.java new file mode 100644 index 000000000000..1066c04f81a5 --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/ConsoleIcon.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.cling.invoker.mvnup; + +import java.nio.charset.Charset; + +import org.jline.terminal.Terminal; + +/** + * Console icons for Maven upgrade tool output. + * Each icon has a Unicode character and an ASCII fallback. + * The appropriate representation is chosen based on the terminal's charset capabilities. + */ +public enum ConsoleIcon { + /** + * Success/completion icon. + */ + SUCCESS('✓', "[OK]"), + + /** + * Error/failure icon. + */ + ERROR('✗', "[ERROR]"), + + /** + * Warning icon. + */ + WARNING('⚠', "[WARNING]"), + + /** + * Detail/bullet point icon. + */ + DETAIL('•', "-"), + + /** + * Action/arrow icon. + */ + ACTION('→', ">"); + + private final char unicodeChar; + private final String asciiFallback; + + ConsoleIcon(char unicodeChar, String asciiFallback) { + this.unicodeChar = unicodeChar; + this.asciiFallback = asciiFallback; + } + + /** + * Returns the appropriate icon representation for the given terminal. + * Tests if the terminal's charset can encode the Unicode character, + * falling back to ASCII if not. + * + * @param terminal the terminal to get the icon for + * @return the Unicode character if supported, otherwise the ASCII fallback + */ + public String getIcon(Terminal terminal) { + Charset charset = getTerminalCharset(terminal); + return charset.newEncoder().canEncode(unicodeChar) ? String.valueOf(unicodeChar) : asciiFallback; + } + + /** + * Gets the charset used by the terminal for output. + * Falls back to the system default charset if terminal charset is not available. + * + * @param terminal the terminal to get the charset from + * @return the terminal's output charset or the system default charset + */ + private static Charset getTerminalCharset(Terminal terminal) { + if (terminal != null && terminal.encoding() != null) { + return terminal.encoding(); + } + return Charset.defaultCharset(); + } + + /** + * Returns the Unicode character for this icon. + * + * @return the Unicode character + */ + public char getUnicodeChar() { + return unicodeChar; + } + + /** + * Returns the ASCII fallback text for this icon. + * + * @return the ASCII fallback text + */ + public String getAsciiFallback() { + return asciiFallback; + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/UpgradeContext.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/UpgradeContext.java index bef4e344fd2c..eef2e59274b4 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/UpgradeContext.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/UpgradeContext.java @@ -118,35 +118,35 @@ public void println() { * Logs a successful operation with a checkmark icon. */ public void success(String message) { - logger.info(getCurrentIndent() + "✓ " + message); + logger.info(getCurrentIndent() + ConsoleIcon.SUCCESS.getIcon(terminal) + " " + message); } /** * Logs an error with an X icon. */ public void failure(String message) { - logger.error(getCurrentIndent() + "✗ " + message); + logger.error(getCurrentIndent() + ConsoleIcon.ERROR.getIcon(terminal) + " " + message); } /** * Logs a warning with a warning icon. */ public void warning(String message) { - logger.warn(getCurrentIndent() + "⚠ " + message); + logger.warn(getCurrentIndent() + ConsoleIcon.WARNING.getIcon(terminal) + " " + message); } /** * Logs detailed information with a bullet point. */ public void detail(String message) { - logger.info(getCurrentIndent() + "• " + message); + logger.info(getCurrentIndent() + ConsoleIcon.DETAIL.getIcon(terminal) + " " + message); } /** * Logs a performed action with an arrow icon. */ public void action(String message) { - logger.info(getCurrentIndent() + "→ " + message); + logger.info(getCurrentIndent() + ConsoleIcon.ACTION.getIcon(terminal) + " " + message); } /** diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java index dfd14967cc1c..b36740613f52 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java @@ -208,10 +208,9 @@ protected int doUpgrade(UpgradeContext context, String targetModel, Map pomMap) { boolean hasChanges = false; - // First apply limited inference (child elements) - hasChanges |= trimParentElementLimited(context, root, parentElement, namespace); - - // Get child GAV + // Get child GAV before applying any changes String childGroupId = getChildText(root, GROUP_ID, namespace); String childVersion = getChildText(root, VERSION, namespace); - // Remove parent groupId if child has no explicit groupId - if (childGroupId == null) { - Element parentGroupIdElement = parentElement.getChild(GROUP_ID, namespace); - if (parentGroupIdElement != null) { - removeElementWithFormatting(parentGroupIdElement); - context.detail("Removed: parent groupId (child has no explicit groupId)"); - hasChanges = true; + // First apply limited inference (child elements) - this removes matching child groupId/version + hasChanges |= trimParentElementLimited(context, root, parentElement, namespace); + + // Only remove parent elements if the parent is in the same reactor (not external) + if (isParentInReactor(parentElement, namespace, pomMap, context)) { + // Remove parent groupId if child has no explicit groupId + if (childGroupId == null) { + Element parentGroupIdElement = parentElement.getChild(GROUP_ID, namespace); + if (parentGroupIdElement != null) { + removeElementWithFormatting(parentGroupIdElement); + context.detail("Removed: parent groupId (child has no explicit groupId)"); + hasChanges = true; + } } - } - // Remove parent version if child has no explicit version - if (childVersion == null) { - Element parentVersionElement = parentElement.getChild(VERSION, namespace); - if (parentVersionElement != null) { - removeElementWithFormatting(parentVersionElement); - context.detail("Removed: parent version (child has no explicit version)"); - hasChanges = true; + // Remove parent version if child has no explicit version + if (childVersion == null) { + Element parentVersionElement = parentElement.getChild(VERSION, namespace); + if (parentVersionElement != null) { + removeElementWithFormatting(parentVersionElement); + context.detail("Removed: parent version (child has no explicit version)"); + hasChanges = true; + } } - } - // Remove parent artifactId if it can be inferred from relativePath - if (canInferParentArtifactId(parentElement, namespace, pomMap)) { - Element parentArtifactIdElement = parentElement.getChild(ARTIFACT_ID, namespace); - if (parentArtifactIdElement != null) { - removeElementWithFormatting(parentArtifactIdElement); - context.detail("Removed: parent artifactId (can be inferred from relativePath)"); - hasChanges = true; + // Remove parent artifactId if it can be inferred from relativePath + if (canInferParentArtifactId(parentElement, namespace, pomMap)) { + Element parentArtifactIdElement = parentElement.getChild(ARTIFACT_ID, namespace); + if (parentArtifactIdElement != null) { + removeElementWithFormatting(parentArtifactIdElement); + context.detail("Removed: parent artifactId (can be inferred from relativePath)"); + hasChanges = true; + } } } return hasChanges; } + /** + * Determines if the parent is part of the same reactor (multi-module project) + * vs. an external parent POM by checking if the parent exists in the pomMap. + */ + private boolean isParentInReactor( + Element parentElement, Namespace namespace, Map pomMap, UpgradeContext context) { + // If relativePath is explicitly set to empty, parent is definitely external + String relativePath = getChildText(parentElement, RELATIVE_PATH, namespace); + if (relativePath != null && relativePath.trim().isEmpty()) { + return false; + } + + // Extract parent GAV + String parentGroupId = getChildText(parentElement, GROUP_ID, namespace); + String parentArtifactId = getChildText(parentElement, ARTIFACT_ID, namespace); + String parentVersion = getChildText(parentElement, VERSION, namespace); + + if (parentGroupId == null || parentArtifactId == null || parentVersion == null) { + // Cannot determine parent GAV, assume external + return false; + } + + GAV parentGAV = new GAV(parentGroupId, parentArtifactId, parentVersion); + + // Check if any POM in our reactor matches the parent GAV using GAVUtils + for (Document pomDocument : pomMap.values()) { + GAV pomGAV = GAVUtils.extractGAVWithParentResolution(context, pomDocument); + if (pomGAV != null && pomGAV.equals(parentGAV)) { + return true; + } + } + + // Parent not found in reactor, must be external + return false; + } + /** * Determines if parent artifactId can be inferred from relativePath. */ @@ -438,11 +477,9 @@ private boolean canInferParentArtifactId(Element parentElement, Namespace namesp relativePath = DEFAULT_PARENT_RELATIVE_PATH; // Maven default } - // For now, we use a simple heuristic: if relativePath is the default "../pom.xml" - // and we have parent POMs in our pomMap, we can likely infer the artifactId. - // A more sophisticated implementation would resolve the actual path and check - // if the parent POM exists in pomMap. - return DEFAULT_PARENT_RELATIVE_PATH.equals(relativePath) && !pomMap.isEmpty(); + // Only infer artifactId if relativePath is the default and we have multiple POMs + // indicating this is likely a multi-module project + return DEFAULT_PARENT_RELATIVE_PATH.equals(relativePath) && pomMap.size() > 1; } /** diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategy.java index 809930044344..aeff300581bf 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategy.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategy.java @@ -114,7 +114,9 @@ public UpgradeResult doApply(UpgradeContext context, Map pomMap) context.success("Model upgrade completed"); modifiedPoms.add(pomPath); } else { - context.warning("Cannot upgrade from " + currentVersion + " to " + targetModelVersion); + // Treat invalid upgrades (including downgrades) as errors, not warnings + context.failure("Cannot upgrade from " + currentVersion + " to " + targetModelVersion); + errorPoms.add(pomPath); } } catch (Exception e) { context.failure("Model upgrade failed: " + e.getMessage()); diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/ConsoleIconTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/ConsoleIconTest.java new file mode 100644 index 000000000000..7b7aadfb2d64 --- /dev/null +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/ConsoleIconTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.cling.invoker.mvnup; + +import java.nio.charset.StandardCharsets; + +import org.jline.terminal.Terminal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the {@link ConsoleIcon} enum. + * Tests icon rendering with different terminal charsets and fallback behavior. + */ +@DisplayName("ConsoleIcon") +class ConsoleIconTest { + + @Test + @DisplayName("should return Unicode icons when terminal supports UTF-8") + void shouldReturnUnicodeWhenTerminalSupportsUtf8() { + Terminal mockTerminal = mock(Terminal.class); + when(mockTerminal.encoding()).thenReturn(StandardCharsets.UTF_8); + + assertEquals("✓", ConsoleIcon.SUCCESS.getIcon(mockTerminal)); + assertEquals("✗", ConsoleIcon.ERROR.getIcon(mockTerminal)); + assertEquals("⚠", ConsoleIcon.WARNING.getIcon(mockTerminal)); + assertEquals("•", ConsoleIcon.DETAIL.getIcon(mockTerminal)); + assertEquals("→", ConsoleIcon.ACTION.getIcon(mockTerminal)); + } + + @Test + @DisplayName("should return ASCII fallback when terminal uses US-ASCII") + void shouldReturnAsciiFallbackWhenTerminalUsesAscii() { + Terminal mockTerminal = mock(Terminal.class); + when(mockTerminal.encoding()).thenReturn(StandardCharsets.US_ASCII); + + assertEquals("[OK]", ConsoleIcon.SUCCESS.getIcon(mockTerminal)); + assertEquals("[ERROR]", ConsoleIcon.ERROR.getIcon(mockTerminal)); + assertEquals("[WARNING]", ConsoleIcon.WARNING.getIcon(mockTerminal)); + assertEquals("-", ConsoleIcon.DETAIL.getIcon(mockTerminal)); + assertEquals(">", ConsoleIcon.ACTION.getIcon(mockTerminal)); + } + + @Test + @DisplayName("should handle null terminal gracefully") + void shouldHandleNullTerminal() { + // Should fall back to system default charset + for (ConsoleIcon icon : ConsoleIcon.values()) { + String result = icon.getIcon(null); + assertNotNull(result, "Icon result should not be null for " + icon); + + // Result should be either Unicode or ASCII fallback depending on default charset + String expectedUnicode = String.valueOf(icon.getUnicodeChar()); + String expectedAscii = icon.getAsciiFallback(); + assertTrue( + result.equals(expectedUnicode) || result.equals(expectedAscii), + "Result should be either Unicode or ASCII fallback for " + icon + ", got: " + result); + } + } + + @Test + @DisplayName("should handle terminal with null encoding") + void shouldHandleTerminalWithNullEncoding() { + Terminal mockTerminal = mock(Terminal.class); + when(mockTerminal.encoding()).thenReturn(null); + + // Should fall back to system default charset + for (ConsoleIcon icon : ConsoleIcon.values()) { + String result = icon.getIcon(mockTerminal); + assertNotNull(result, "Icon result should not be null for " + icon); + + // Result should be either Unicode or ASCII fallback depending on default charset + String expectedUnicode = String.valueOf(icon.getUnicodeChar()); + String expectedAscii = icon.getAsciiFallback(); + assertTrue( + result.equals(expectedUnicode) || result.equals(expectedAscii), + "Result should be either Unicode or ASCII fallback for " + icon + ", got: " + result); + } + } + + @Test + @DisplayName("should return correct Unicode characters") + void shouldReturnCorrectUnicodeCharacters() { + assertEquals('✓', ConsoleIcon.SUCCESS.getUnicodeChar()); + assertEquals('✗', ConsoleIcon.ERROR.getUnicodeChar()); + assertEquals('⚠', ConsoleIcon.WARNING.getUnicodeChar()); + assertEquals('•', ConsoleIcon.DETAIL.getUnicodeChar()); + assertEquals('→', ConsoleIcon.ACTION.getUnicodeChar()); + } + + @Test + @DisplayName("should return correct ASCII fallbacks") + void shouldReturnCorrectAsciiFallbacks() { + assertEquals("[OK]", ConsoleIcon.SUCCESS.getAsciiFallback()); + assertEquals("[ERROR]", ConsoleIcon.ERROR.getAsciiFallback()); + assertEquals("[WARNING]", ConsoleIcon.WARNING.getAsciiFallback()); + assertEquals("-", ConsoleIcon.DETAIL.getAsciiFallback()); + assertEquals(">", ConsoleIcon.ACTION.getAsciiFallback()); + } + + @Test + @DisplayName("should handle different charset encodings correctly") + void shouldHandleDifferentCharsetEncodingsCorrectly() { + Terminal mockTerminal = mock(Terminal.class); + + // Test with ISO-8859-1 (Latin-1) - should support some but not all Unicode chars + when(mockTerminal.encoding()).thenReturn(StandardCharsets.ISO_8859_1); + + for (ConsoleIcon icon : ConsoleIcon.values()) { + String result = icon.getIcon(mockTerminal); + assertNotNull(result, "Icon result should not be null for " + icon); + + // Result should be consistent with charset's canEncode capability + boolean canEncode = StandardCharsets.ISO_8859_1.newEncoder().canEncode(icon.getUnicodeChar()); + String expected = canEncode ? String.valueOf(icon.getUnicodeChar()) : icon.getAsciiFallback(); + assertEquals(expected, result, "Icon should match charset encoding capability for " + icon); + } + } + + @Test + @DisplayName("should be consistent across multiple calls") + void shouldBeConsistentAcrossMultipleCalls() { + Terminal mockTerminal = mock(Terminal.class); + when(mockTerminal.encoding()).thenReturn(StandardCharsets.UTF_8); + + for (ConsoleIcon icon : ConsoleIcon.values()) { + String firstCall = icon.getIcon(mockTerminal); + String secondCall = icon.getIcon(mockTerminal); + assertEquals(firstCall, secondCall, "Icon should be consistent across calls for " + icon); + } + } +} diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/UpgradeContextTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/UpgradeContextTest.java new file mode 100644 index 000000000000..c1c7d82ff54a --- /dev/null +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/UpgradeContextTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.cling.invoker.mvnup; + +import java.nio.file.Paths; + +import org.apache.maven.cling.invoker.mvnup.goals.TestUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Unit tests for the {@link UpgradeContext} class. + * Tests console output formatting and Unicode icon fallback behavior. + */ +@DisplayName("UpgradeContext") +class UpgradeContextTest { + + @Test + @DisplayName("should create context successfully") + void shouldCreateContextSuccessfully() { + // Use existing test utilities to create a context + UpgradeContext context = TestUtils.createMockContext(Paths.get("/test")); + + // Verify context is created and basic methods work + assertNotNull(context, "Context should be created"); + assertNotNull(context.options(), "Options should be available"); + + // Test that icon methods don't throw exceptions + // (The actual icon choice depends on terminal charset capabilities) + context.success("Test success message"); + context.failure("Test failure message"); + context.warning("Test warning message"); + context.detail("Test detail message"); + context.action("Test action message"); + } + + @Test + @DisplayName("should handle indentation correctly") + void shouldHandleIndentationCorrectly() { + UpgradeContext context = TestUtils.createMockContext(Paths.get("/test")); + + // Test indentation methods don't throw exceptions + context.indent(); + context.indent(); + context.info("Indented message"); + + context.unindent(); + context.unindent(); + context.unindent(); // Should not go below 0 + context.info("Unindented message"); + } + + @Test + @DisplayName("should handle icon rendering based on terminal capabilities") + void shouldHandleIconRenderingBasedOnTerminalCapabilities() { + UpgradeContext context = TestUtils.createMockContext(Paths.get("/test")); + + // Test that icon rendering doesn't throw exceptions + // The actual icons used depend on the terminal's charset capabilities + context.success("Icon rendering test"); + context.failure("Icon rendering test"); + context.warning("Icon rendering test"); + context.detail("Icon rendering test"); + context.action("Icon rendering test"); + + // We just verify the methods work without throwing exceptions + // The specific icons (Unicode vs ASCII) depend on terminal charset + } +} diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoalTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoalTest.java index 59a4b720e880..fba9ce3e46fa 100644 --- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoalTest.java +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoalTest.java @@ -39,7 +39,6 @@ import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -189,8 +188,8 @@ void shouldCreateMvnDirectoryWhenModelVersionNot410() throws Exception { } @Test - @DisplayName("should not create .mvn directory when model version is 4.1.0") - void shouldNotCreateMvnDirectoryWhenModelVersion410() throws Exception { + @DisplayName("should create .mvn directory when model version is 4.1.0") + void shouldCreateMvnDirectoryWhenModelVersion410() throws Exception { Path projectDir = tempDir.resolve("project"); Files.createDirectories(projectDir); @@ -200,11 +199,13 @@ void shouldNotCreateMvnDirectoryWhenModelVersion410() throws Exception { when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) .thenReturn(UpgradeResult.empty()); - // Execute with target model 4.1.0 (should not create .mvn directory) + // Execute with target model 4.1.0 (should create .mvn directory to avoid root warnings) upgradeGoal.testExecuteWithTargetModel(context, "4.1.0"); Path mvnDir = projectDir.resolve(".mvn"); - assertFalse(Files.exists(mvnDir), ".mvn directory should not be created for 4.1.0"); + assertTrue( + Files.exists(mvnDir), + ".mvn directory should be created for 4.1.0 to avoid root directory warnings"); } @Test diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/InferenceStrategyTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/InferenceStrategyTest.java index 26e8d6fc2ec1..766c30be58b3 100644 --- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/InferenceStrategyTest.java +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/InferenceStrategyTest.java @@ -484,9 +484,66 @@ void shouldNotTrimParentElementsWhenParentIsExternal() throws Exception { strategy.apply(context, pomMap); // Verify correct behavior for external parent: - // - groupId should be removed (child doesn't have explicit groupId, can inherit from parent) - // - version should be removed (child doesn't have explicit version, can inherit from parent) - // - artifactId should be removed (Maven 4.1.0+ can infer from relativePath even for external parents) + // - groupId should NOT be removed (external parents need groupId to be located) + // - artifactId should NOT be removed (external parents need artifactId to be located) + // - version should NOT be removed (external parents need version to be located) + // This prevents the "parent.groupId is missing" error reported in issue #7934 + assertNotNull(parentElement.getChild("groupId", childRoot.getNamespace())); + assertNotNull(parentElement.getChild("artifactId", childRoot.getNamespace())); + assertNotNull(parentElement.getChild("version", childRoot.getNamespace())); + } + + @Test + @DisplayName("should trim parent elements when parent is in reactor") + void shouldTrimParentElementsWhenParentIsInReactor() throws Exception { + // Create parent POM + String parentPomXml = + """ + + + 4.1.0 + com.example + parent-project + 1.0.0 + pom + + """; + + // Create child POM that references the parent + String childPomXml = + """ + + + 4.1.0 + + com.example + parent-project + 1.0.0 + + child-project + + + """; + + Document parentDoc = saxBuilder.build(new StringReader(parentPomXml)); + Document childDoc = saxBuilder.build(new StringReader(childPomXml)); + + // Both POMs are in the reactor + Map pomMap = Map.of( + Paths.get("pom.xml"), parentDoc, + Paths.get("child", "pom.xml"), childDoc); + + Element childRoot = childDoc.getRootElement(); + Element parentElement = childRoot.getChild("parent", childRoot.getNamespace()); + + // Apply inference + UpgradeContext context = createMockContext(); + strategy.apply(context, pomMap); + + // Verify correct behavior for reactor parent: + // - groupId should be removed (child has no explicit groupId, parent is in reactor) + // - artifactId should be removed (can be inferred from relativePath) + // - version should be removed (child has no explicit version, parent is in reactor) assertNull(parentElement.getChild("groupId", childRoot.getNamespace())); assertNull(parentElement.getChild("artifactId", childRoot.getNamespace())); assertNull(parentElement.getChild("version", childRoot.getNamespace())); diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategyTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategyTest.java index 4ca99a9301d5..2a0c3c171980 100644 --- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategyTest.java +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModelUpgradeStrategyTest.java @@ -323,4 +323,63 @@ void shouldProvideMeaningfulDescription() { "Description should mention model or upgrade"); } } + + @Nested + @DisplayName("Downgrade Handling") + class DowngradeHandlingTests { + + @Test + @DisplayName("should fail with error when attempting downgrade from 4.1.0 to 4.0.0") + void shouldFailWhenAttemptingDowngrade() throws Exception { + String pomXml = + """ + + + 4.1.0 + com.example + test-project + 1.0.0 + + """; + + Document document = saxBuilder.build(new StringReader(pomXml)); + Map pomMap = Map.of(Paths.get("pom.xml"), document); + + UpgradeContext context = TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.0.0")); + + UpgradeResult result = strategy.apply(context, pomMap); + + // Should have errors (not just warnings) + assertTrue(result.errorCount() > 0, "Downgrade should result in errors"); + assertFalse(result.success(), "Downgrade should not be successful"); + assertEquals(1, result.errorCount(), "Should have exactly one error"); + } + + @Test + @DisplayName("should succeed when upgrading from 4.0.0 to 4.1.0") + void shouldSucceedWhenUpgrading() throws Exception { + String pomXml = + """ + + + 4.0.0 + com.example + test-project + 1.0.0 + + """; + + Document document = saxBuilder.build(new StringReader(pomXml)); + Map pomMap = Map.of(Paths.get("pom.xml"), document); + + UpgradeContext context = TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0")); + + UpgradeResult result = strategy.apply(context, pomMap); + + // Should succeed + assertTrue(result.success(), "Valid upgrade should be successful"); + assertEquals(0, result.errorCount(), "Should have no errors"); + assertEquals(1, result.modifiedCount(), "Should have modified one POM"); + } + } } diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/UpgradeWorkflowIntegrationTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/UpgradeWorkflowIntegrationTest.java index eb2386530c12..5a652d557ff7 100644 --- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/UpgradeWorkflowIntegrationTest.java +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/UpgradeWorkflowIntegrationTest.java @@ -30,7 +30,6 @@ import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -94,8 +93,8 @@ void shouldUpgradeModelVersionWith41Option() throws Exception { } @Test - @DisplayName("should not create .mvn directory when upgrading to 4.1.0") - void shouldNotCreateMvnDirectoryFor41Upgrade() throws Exception { + @DisplayName("should create .mvn directory when upgrading to 4.1.0") + void shouldCreateMvnDirectoryFor41Upgrade() throws Exception { Path pomFile = tempDir.resolve("pom.xml"); String originalPom = PomBuilder.create() .groupId("com.example") @@ -110,7 +109,9 @@ void shouldNotCreateMvnDirectoryFor41Upgrade() throws Exception { applyGoal.execute(context); Path mvnDir = tempDir.resolve(".mvn"); - assertFalse(Files.exists(mvnDir), ".mvn directory should not be created for 4.1.0 upgrade"); + assertTrue( + Files.exists(mvnDir), + ".mvn directory should be created for 4.1.0 upgrade to avoid root directory warnings"); } }