SmartProjectComparatorTest.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.lifecycle.internal.builder.multithreaded;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.maven.execution.ProjectDependencyGraph;
import org.apache.maven.lifecycle.internal.stub.ProjectDependencyGraphStub;
import org.apache.maven.project.MavenProject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Test for SmartProjectComparator to verify critical path scheduling logic.
 */
class SmartProjectComparatorTest {

    private SmartProjectComparator comparator;
    private ProjectDependencyGraph dependencyGraph;

    @BeforeEach
    void setUp() {
        dependencyGraph = new ProjectDependencyGraphStub();
        comparator = new SmartProjectComparator(dependencyGraph);
    }

    @Test
    void testProjectWeightCalculation() {
        // Test that projects with longer downstream chains get higher weights
        // Graph: A -> B,C; B -> X,Y; C -> X,Z
        MavenProject projectA = ProjectDependencyGraphStub.A;
        MavenProject projectB = ProjectDependencyGraphStub.B;
        MavenProject projectC = ProjectDependencyGraphStub.C;
        MavenProject projectX = ProjectDependencyGraphStub.X;

        long weightA = comparator.getProjectWeight(projectA);
        long weightB = comparator.getProjectWeight(projectB);
        long weightC = comparator.getProjectWeight(projectC);
        long weightX = comparator.getProjectWeight(projectX);

        // Project A should have the highest weight as it's at the root
        assertTrue(weightA > weightB, "Project A should have weight > Project B");
        assertTrue(weightA > weightC, "Project A should have weight > Project C");
        assertTrue(weightB > weightX, "Project B should have weight > Project X");
        assertTrue(weightC > weightX, "Project C should have weight > Project X");
    }

    @Test
    void testComparatorOrdering() {
        List<MavenProject> projects = Arrays.asList(
                ProjectDependencyGraphStub.X,
                ProjectDependencyGraphStub.C,
                ProjectDependencyGraphStub.A,
                ProjectDependencyGraphStub.B);

        // Sort using the comparator
        projects.sort(comparator.getComparator());

        // Project A should come first (highest weight)
        assertEquals(
                ProjectDependencyGraphStub.A,
                projects.get(0),
                "Project A should be first (highest critical path weight)");

        // B and C should come before X (they have higher weights)
        assertTrue(
                projects.indexOf(ProjectDependencyGraphStub.B) < projects.indexOf(ProjectDependencyGraphStub.X),
                "Project B should come before X");
        assertTrue(
                projects.indexOf(ProjectDependencyGraphStub.C) < projects.indexOf(ProjectDependencyGraphStub.X),
                "Project C should come before X");
    }

    @Test
    void testWeightConsistency() {
        // Test that weights are consistent across multiple calls
        MavenProject project = ProjectDependencyGraphStub.A;

        long weight1 = comparator.getProjectWeight(project);
        long weight2 = comparator.getProjectWeight(project);

        assertEquals(weight1, weight2, "Project weight should be consistent");
    }

    @Test
    void testDependencyChainLength() {
        // Test that projects with longer dependency chains get higher weights
        // In the stub: A -> B,C; B -> X,Y; C -> X,Z
        long weightA = comparator.getProjectWeight(ProjectDependencyGraphStub.A);
        long weightB = comparator.getProjectWeight(ProjectDependencyGraphStub.B);
        long weightC = comparator.getProjectWeight(ProjectDependencyGraphStub.C);
        long weightX = comparator.getProjectWeight(ProjectDependencyGraphStub.X);
        long weightY = comparator.getProjectWeight(ProjectDependencyGraphStub.Y);
        long weightZ = comparator.getProjectWeight(ProjectDependencyGraphStub.Z);

        // Verify the actual chain length calculation
        // Leaf nodes (no downstream dependencies)
        assertEquals(1L, weightX, "Project X should have weight 1 (1 + 0)");
        assertEquals(1L, weightY, "Project Y should have weight 1 (1 + 0)");
        assertEquals(1L, weightZ, "Project Z should have weight 1 (1 + 0)");

        // Middle nodes
        assertEquals(2L, weightB, "Project B should have weight 2 (1 + max(X=1, Y=1))");
        assertEquals(2L, weightC, "Project C should have weight 2 (1 + max(X=1, Z=1))");

        // Root node
        assertEquals(3L, weightA, "Project A should have weight 3 (1 + max(B=2, C=2))");
    }

    @Test
    void testSameWeightOrdering() {
        // Test that projects with the same weight are ordered by project ID
        // Projects B and C both have weight 2, so they should be ordered by project ID
        List<MavenProject> projects = Arrays.asList(
                ProjectDependencyGraphStub.C, // weight=2, ID contains "C"
                ProjectDependencyGraphStub.B // weight=2, ID contains "B"
                );

        projects.sort(comparator.getComparator());

        // Both have same weight (2), so ordering should be by project ID
        // Project B should come before C alphabetically by project ID
        assertEquals(
                ProjectDependencyGraphStub.B,
                projects.get(0),
                "Project B should come before C when they have the same weight (ordered by project ID)");
        assertEquals(
                ProjectDependencyGraphStub.C,
                projects.get(1),
                "Project C should come after B when they have the same weight (ordered by project ID)");

        // Verify they actually have the same weight
        long weightB = comparator.getProjectWeight(ProjectDependencyGraphStub.B);
        long weightC = comparator.getProjectWeight(ProjectDependencyGraphStub.C);
        assertEquals(weightB, weightC, "Projects B and C should have the same weight");
    }

    @Test
    void testConcurrentWeightCalculation() throws Exception {
        // Test that concurrent weight calculation doesn't cause recursive update issues
        // This test simulates the scenario that causes the IllegalStateException

        int numThreads = 10;
        int numIterations = 100;
        ExecutorService executor = Executors.newFixedThreadPool(numThreads);
        CountDownLatch latch = new CountDownLatch(numThreads);
        AtomicReference<Exception> exception = new AtomicReference<>();

        for (int i = 0; i < numThreads; i++) {
            executor.submit(() -> {
                try {
                    for (int j = 0; j < numIterations; j++) {
                        // Simulate concurrent access to weight calculation
                        // This can trigger the recursive update issue
                        List<MavenProject> projects = Arrays.asList(
                                ProjectDependencyGraphStub.A,
                                ProjectDependencyGraphStub.B,
                                ProjectDependencyGraphStub.C,
                                ProjectDependencyGraphStub.X,
                                ProjectDependencyGraphStub.Y,
                                ProjectDependencyGraphStub.Z);

                        // Sort projects concurrently - this triggers weight calculation
                        projects.sort(comparator.getComparator());

                        // Also directly access weights to increase contention
                        for (MavenProject project : projects) {
                            comparator.getProjectWeight(project);
                        }
                    }
                } catch (Exception e) {
                    exception.set(e);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await(30, TimeUnit.SECONDS);
        executor.shutdown();

        if (exception.get() != null) {
            throw exception.get();
        }
    }
}