ParentCycleDetectionTest.java

/*
 * 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.impl.model;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.apache.maven.api.Session;
import org.apache.maven.api.services.ModelBuilder;
import org.apache.maven.api.services.ModelBuilderException;
import org.apache.maven.api.services.ModelBuilderRequest;
import org.apache.maven.api.services.ModelBuilderResult;
import org.apache.maven.api.services.Sources;
import org.apache.maven.impl.standalone.ApiRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;

/**
 * Test for parent resolution cycle detection.
 */
class ParentCycleDetectionTest {

    Session session;
    ModelBuilder modelBuilder;

    @BeforeEach
    void setup() {
        session = ApiRunner.createSession();
        modelBuilder = session.getService(ModelBuilder.class);
        assertNotNull(modelBuilder);
    }

    @Test
    void testParentResolutionCycleDetectionWithRelativePath(@TempDir Path tempDir) throws IOException {
        // Create .mvn directory to mark root
        Files.createDirectories(tempDir.resolve(".mvn"));

        // Create a parent resolution cycle using relativePath: child -> parent -> child
        // This reproduces the same issue as the integration test MavenITmng11009StackOverflowParentResolutionTest
        Path childPom = tempDir.resolve("pom.xml");
        Files.writeString(
                childPom,
                """
            <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
                <modelVersion>4.0.0</modelVersion>
                <parent>
                    <groupId>org.apache.maven.its.mng11009</groupId>
                    <artifactId>parent</artifactId>
                    <version>1.0-SNAPSHOT</version>
                    <relativePath>parent</relativePath>
                </parent>
                <artifactId>child</artifactId>
                <packaging>pom</packaging>
            </project>
            """);

        Path parentPom = tempDir.resolve("parent").resolve("pom.xml");
        Files.createDirectories(parentPom.getParent());
        Files.writeString(
                parentPom,
                """
            <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
                <modelVersion>4.0.0</modelVersion>
                <parent>
                    <groupId>org.apache.maven.its.mng11009</groupId>
                    <artifactId>external-parent</artifactId>
                    <version>1.0-SNAPSHOT</version>
                    <!-- No relativePath specified, defaults to ../pom.xml which creates the circular reference -->
                </parent>
                <artifactId>parent</artifactId>
                <packaging>pom</packaging>
            </project>
            """);

        ModelBuilderRequest request = ModelBuilderRequest.builder()
                .session(session)
                .source(Sources.buildSource(childPom))
                .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
                .build();

        // This should either:
        // 1. Detect the cycle and throw a meaningful ModelBuilderException, OR
        // 2. Not cause a StackOverflowError (the main goal is to prevent the StackOverflowError)
        try {
            ModelBuilderResult result = modelBuilder.newSession().build(request);
            // If we get here without StackOverflowError, that's actually good progress
            // The build may still fail with a different error (circular dependency), but that's expected
            System.out.println("Build completed without StackOverflowError. Result: " + result);
        } catch (StackOverflowError error) {
            fail(
                    "Build failed with StackOverflowError, which should be prevented. This indicates the cycle detection is not working properly for relativePath-based cycles.");
        } catch (ModelBuilderException exception) {
            // This is acceptable - the build should fail with a meaningful error, not StackOverflowError
            System.out.println("Build failed with ModelBuilderException (expected): " + exception.getMessage());
            // Check if it's a cycle detection error
            if (exception.getMessage().contains("cycle")
                    || exception.getMessage().contains("circular")) {
                System.out.println("��� Cycle detected correctly!");
            }
            // We don't assert on the specific message because the main goal is to prevent StackOverflowError
        }
    }

    @Test
    void testDirectCycleDetection(@TempDir Path tempDir) throws IOException {
        // Create .mvn directory to mark root
        Files.createDirectories(tempDir.resolve(".mvn"));

        // Create a direct cycle: A -> B -> A
        Path pomA = tempDir.resolve("a").resolve("pom.xml");
        Files.createDirectories(pomA.getParent());
        Files.writeString(
                pomA,
                """
            <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
                <modelVersion>4.0.0</modelVersion>
                <groupId>test</groupId>
                <artifactId>a</artifactId>
                <version>1.0</version>
                <parent>
                    <groupId>test</groupId>
                    <artifactId>b</artifactId>
                    <version>1.0</version>
                    <relativePath>../b/pom.xml</relativePath>
                </parent>
            </project>
            """);

        Path pomB = tempDir.resolve("b").resolve("pom.xml");
        Files.createDirectories(pomB.getParent());
        Files.writeString(
                pomB,
                """
            <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
                <modelVersion>4.0.0</modelVersion>
                <groupId>test</groupId>
                <artifactId>b</artifactId>
                <version>1.0</version>
                <parent>
                    <groupId>test</groupId>
                    <artifactId>a</artifactId>
                    <version>1.0</version>
                    <relativePath>../a/pom.xml</relativePath>
                </parent>
            </project>
            """);

        ModelBuilderRequest request = ModelBuilderRequest.builder()
                .session(session)
                .source(Sources.buildSource(pomA))
                .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
                .build();

        // This should detect the cycle and throw a meaningful ModelBuilderException
        try {
            ModelBuilderResult result = modelBuilder.newSession().build(request);
            fail("Expected ModelBuilderException due to cycle detection, but build succeeded: " + result);
        } catch (StackOverflowError error) {
            fail("Build failed with StackOverflowError, which should be prevented by cycle detection.");
        } catch (ModelBuilderException exception) {
            // This is expected - the build should fail with a cycle detection error
            System.out.println("Build failed with ModelBuilderException (expected): " + exception.getMessage());
            // Check if it's a cycle detection error
            if (exception.getMessage().contains("cycle")
                    || exception.getMessage().contains("circular")) {
                System.out.println("��� Cycle detected correctly!");
            } else {
                System.out.println("��� Exception was not a cycle detection error: " + exception.getMessage());
            }
        }
    }

    @Test
    void testMultipleModulesWithSameParentDoNotCauseCycle(@TempDir Path tempDir) throws IOException {
        // Create .mvn directory to mark root
        Files.createDirectories(tempDir.resolve(".mvn"));

        // Create a scenario like the failing test: multiple modules with the same parent
        Path parentPom = tempDir.resolve("parent").resolve("pom.xml");
        Files.createDirectories(parentPom.getParent());
        Files.writeString(
                parentPom,
                """
            <project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
                <modelVersion>4.1.0</modelVersion>
                <groupId>test</groupId>
                <artifactId>parent</artifactId>
                <version>1.0</version>
                <packaging>pom</packaging>
            </project>
            """);

        Path moduleA = tempDir.resolve("module-a").resolve("pom.xml");
        Files.createDirectories(moduleA.getParent());
        Files.writeString(
                moduleA,
                """
            <project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
                <modelVersion>4.1.0</modelVersion>
                <parent>
                    <groupId>test</groupId>
                    <artifactId>parent</artifactId>
                    <version>1.0</version>
                    <relativePath>../parent/pom.xml</relativePath>
                </parent>
                <artifactId>module-a</artifactId>
            </project>
            """);

        Path moduleB = tempDir.resolve("module-b").resolve("pom.xml");
        Files.createDirectories(moduleB.getParent());
        Files.writeString(
                moduleB,
                """
            <project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
                <modelVersion>4.1.0</modelVersion>
                <parent>
                    <groupId>test</groupId>
                    <artifactId>parent</artifactId>
                    <version>1.0</version>
                    <relativePath>../parent/pom.xml</relativePath>
                </parent>
                <artifactId>module-b</artifactId>
            </project>
            """);

        // Both modules should be able to resolve their parent without cycle detection errors
        ModelBuilderRequest requestA = ModelBuilderRequest.builder()
                .session(session)
                .source(Sources.buildSource(moduleA))
                .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
                .build();

        ModelBuilderRequest requestB = ModelBuilderRequest.builder()
                .session(session)
                .source(Sources.buildSource(moduleB))
                .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
                .build();

        // These should not throw exceptions
        ModelBuilderResult resultA = modelBuilder.newSession().build(requestA);
        ModelBuilderResult resultB = modelBuilder.newSession().build(requestB);

        // Verify that both models were built successfully
        assertNotNull(resultA);
        assertNotNull(resultB);
    }
}