MavenExecutorTestSupport.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.cling.executor;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;

import eu.maveniverse.maven.mimir.testing.MimirInfuser;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.cli.Executor;
import org.apache.maven.api.cli.ExecutorRequest;
import org.apache.maven.cling.executor.embedded.EmbeddedMavenExecutor;
import org.apache.maven.cling.executor.forked.ForkedMavenExecutor;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.io.CleanupMode;
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;
import static org.junit.jupiter.api.condition.OS.WINDOWS;

public abstract class MavenExecutorTestSupport {
    @Timeout(15)
    @Test
    void mvnenc(
            @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path cwd,
            @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path userHome)
            throws Exception {
        String logfile = "m4.log";
        execute(
                cwd.resolve(logfile),
                List.of(mvn4ExecutorRequestBuilder()
                        .command("mvnenc")
                        .cwd(cwd)
                        .userHomeDirectory(userHome)
                        .argument("diag")
                        .argument("-l")
                        .argument(logfile)
                        .build()));
        System.out.println(Files.readString(cwd.resolve(logfile)));
    }

    @DisabledOnOs(
            value = WINDOWS,
            disabledReason = "JUnit on Windows fails to clean up as mvn3 does not close log file properly")
    @Timeout(15)
    @Test
    void dump3(
            @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path cwd,
            @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path userHome)
            throws Exception {
        String logfile = "m3.log";
        execute(
                cwd.resolve(logfile),
                List.of(mvn3ExecutorRequestBuilder()
                        .cwd(cwd)
                        .userHomeDirectory(userHome)
                        .argument("eu.maveniverse.maven.plugins:toolbox:0.7.4:gav-dump")
                        .argument("-l")
                        .argument(logfile)
                        .build()));
        System.out.println(Files.readString(cwd.resolve(logfile)));
    }

    @Timeout(15)
    @Test
    void dump4(
            @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path cwd,
            @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path userHome)
            throws Exception {
        String logfile = "m4.log";
        execute(
                cwd.resolve(logfile),
                List.of(mvn4ExecutorRequestBuilder()
                        .cwd(cwd)
                        .userHomeDirectory(userHome)
                        .argument("eu.maveniverse.maven.plugins:toolbox:0.7.4:gav-dump")
                        .argument("-l")
                        .argument(logfile)
                        .build()));
        System.out.println(Files.readString(cwd.resolve(logfile)));
    }

    @Timeout(15)
    @Test
    void defaultFs(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception {
        layDownFiles(tempDir);
        String logfile = "m4.log";
        execute(
                tempDir.resolve(logfile),
                List.of(mvn4ExecutorRequestBuilder()
                        .cwd(tempDir)
                        .argument("-V")
                        .argument("verify")
                        .argument("-l")
                        .argument(logfile)
                        .build()));
    }

    @Timeout(15)
    @Test
    void version() throws Exception {
        assertEquals(
                System.getProperty("maven4version"),
                mavenVersion(mvn4ExecutorRequestBuilder().build()));
    }

    @DisabledOnOs(
            value = WINDOWS,
            disabledReason = "JUnit on Windows fails to clean up as mvn3 does not close log file properly")
    @Timeout(15)
    @Test
    void defaultFs3x(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception {
        layDownFiles(tempDir);
        String logfile = "m3.log";
        execute(
                tempDir.resolve(logfile),
                List.of(mvn3ExecutorRequestBuilder()
                        .cwd(tempDir)
                        .argument("-V")
                        .argument("verify")
                        .argument("-l")
                        .argument(logfile)
                        .build()));
    }

    @Timeout(15)
    @Test
    void version3x() throws Exception {
        assertEquals(
                System.getProperty("maven3version"),
                mavenVersion(mvn3ExecutorRequestBuilder().build()));
    }

    @Timeout(15)
    @Test
    void defaultFsCaptureOutput(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception {
        layDownFiles(tempDir);
        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        execute(
                null,
                List.of(mvn4ExecutorRequestBuilder()
                        .cwd(tempDir)
                        .argument("-V")
                        .argument("verify")
                        .stdOut(stdout)
                        .build()));
        assertFalse(stdout.toString().contains("[\u001B["), "By default no ANSI color codes");
        assertTrue(stdout.toString().contains("INFO"), "No INFO found");
    }

    @Timeout(15)
    @Test
    void defaultFsCaptureOutputWithForcedColor(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir)
            throws Exception {
        layDownFiles(tempDir);
        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        execute(
                null,
                List.of(mvn4ExecutorRequestBuilder()
                        .cwd(tempDir)
                        .argument("-V")
                        .argument("verify")
                        .argument("--color=yes")
                        .stdOut(stdout)
                        .build()));
        assertTrue(stdout.toString().contains("[\u001B["), "No ANSI codes present");
        assertTrue(stdout.toString().contains("INFO"), "No INFO found");
    }

    @Timeout(15)
    @Test
    void defaultFsCaptureOutputWithForcedOffColor(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir)
            throws Exception {
        layDownFiles(tempDir);
        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        execute(
                null,
                List.of(mvn4ExecutorRequestBuilder()
                        .cwd(tempDir)
                        .argument("-V")
                        .argument("verify")
                        .argument("--color=no")
                        .stdOut(stdout)
                        .build()));
        assertFalse(stdout.toString().contains("[\u001B["), "No ANSI codes present");
        assertTrue(stdout.toString().contains("INFO"), "No INFO found");
    }

    @Timeout(15)
    @Test
    void defaultFs3xCaptureOutput(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception {
        layDownFiles(tempDir);
        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        execute(
                null,
                List.of(mvn3ExecutorRequestBuilder()
                        .cwd(tempDir)
                        .argument("-V")
                        .argument("verify")
                        .stdOut(stdout)
                        .build()));
        // Note: we do not validate ANSI as Maven3 is weird in this respect (thinks is color but is not)
        // assertTrue(stdout.toString().contains("[\u001B["), "No ANSI codes present");
        assertTrue(stdout.toString().contains("INFO"), "No INFO found");
    }

    @Timeout(15)
    @Test
    void defaultFs3xCaptureOutputWithForcedColor(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir)
            throws Exception {
        layDownFiles(tempDir);
        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        execute(
                null,
                List.of(mvn3ExecutorRequestBuilder()
                        .cwd(tempDir)
                        .argument("-V")
                        .argument("verify")
                        .argument("--color=yes")
                        .stdOut(stdout)
                        .build()));
        assertTrue(stdout.toString().contains("[\u001B["), "No ANSI codes present");
        assertTrue(stdout.toString().contains("INFO"), "No INFO found");
    }

    @Timeout(15)
    @Test
    void defaultFs3xCaptureOutputWithForcedOffColor(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir)
            throws Exception {
        layDownFiles(tempDir);
        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        execute(
                null,
                List.of(mvn3ExecutorRequestBuilder()
                        .cwd(tempDir)
                        .argument("-V")
                        .argument("verify")
                        .argument("--color=no")
                        .stdOut(stdout)
                        .build()));
        assertFalse(stdout.toString().contains("[\u001B["), "No ANSI codes present");
        assertTrue(stdout.toString().contains("INFO"), "No INFO found");
    }

    public static final String POM_STRING =
            """
                <?xml version="1.0" encoding="UTF-8"?>
                <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 https://maven.apache.org/maven-v4_0_0.xsd">

                    <modelVersion>4.0.0</modelVersion>

                    <groupId>org.apache.maven.samples</groupId>
                    <artifactId>sample</artifactId>
                    <version>1.0.0</version>

                    <dependencyManagement>
                      <dependencies>
                        <dependency>
                          <groupId>org.junit</groupId>
                          <artifactId>junit-bom</artifactId>
                          <version>5.11.1</version>
                          <type>pom</type>
                          <scope>import</scope>
                        </dependency>
                      </dependencies>
                    </dependencyManagement>

                    <dependencies>
                      <dependency>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter-api</artifactId>
                        <scope>test</scope>
                      </dependency>
                    </dependencies>

                </project>
                """;

    public static final String APP_JAVA_STRING =
            """
            package org.apache.maven.samples.sample;

            public class App {
                public static void main(String... args) {
                    System.out.println("Hello World!");
                }
            }
            """;

    protected void execute(@Nullable Path logFile, Collection<ExecutorRequest> requests) throws Exception {
        Executor invoker = createAndMemoizeExecutor();
        for (ExecutorRequest request : requests) {
            MimirInfuser.infuseUW(request.userHomeDirectory());
            int exitCode = invoker.execute(request);
            if (exitCode != 0) {
                throw new FailedExecution(request, exitCode, logFile == null ? "" : Files.readString(logFile));
            }
        }
    }

    protected String mavenVersion(ExecutorRequest request) throws Exception {
        return createAndMemoizeExecutor().mavenVersion(request);
    }

    public static ExecutorRequest.Builder mvn3ExecutorRequestBuilder() {
        return addTailRepo(ExecutorRequest.mavenBuilder(Paths.get(System.getProperty("maven3home"))));
    }

    public static ExecutorRequest.Builder mvn4ExecutorRequestBuilder() {
        return addTailRepo(ExecutorRequest.mavenBuilder(Paths.get(System.getProperty("maven4home"))));
    }

    private static ExecutorRequest.Builder addTailRepo(ExecutorRequest.Builder builder) {
        if (System.getProperty("localRepository") != null) {
            builder.argument("-Dmaven.repo.local.tail=" + System.getProperty("localRepository"));
        }
        return builder;
    }

    protected void layDownFiles(Path cwd) throws IOException {
        Files.createDirectory(cwd.resolve(".mvn"));
        Path pom = cwd.resolve("pom.xml").toAbsolutePath();
        Files.writeString(pom, POM_STRING);
        Path appJava = cwd.resolve("src/main/java/org/apache/maven/samples/sample/App.java");
        Files.createDirectories(appJava.getParent());
        Files.writeString(appJava, APP_JAVA_STRING);
    }

    protected static class FailedExecution extends Exception {
        private final ExecutorRequest request;
        private final int exitCode;
        private final String log;

        public FailedExecution(ExecutorRequest request, int exitCode, String log) {
            super(request.toString() + " => " + exitCode + "\n" + log);
            this.request = request;
            this.exitCode = exitCode;
            this.log = log;
        }

        public ExecutorRequest getRequest() {
            return request;
        }

        public int getExitCode() {
            return exitCode;
        }

        public String getLog() {
            return log;
        }
    }

    private static Executor executor;

    protected final Executor createAndMemoizeExecutor() {
        if (executor == null) {
            executor = doSelectExecutor();
        }
        return executor;
    }

    @AfterAll
    static void afterAll() {
        if (executor != null) {
            executor = null;
        }
    }

    // NOTE: we keep these instances alive to make sure JVM (running tests) loads JAnsi/JLine native library ONLY once
    // in real life you'd anyway keep these alive as long needed, but here, we repeat a series of tests against same
    // instance, to prevent them attempting native load more than once.
    public static final EmbeddedMavenExecutor EMBEDDED_MAVEN_EXECUTOR = new EmbeddedMavenExecutor();
    public static final ForkedMavenExecutor FORKED_MAVEN_EXECUTOR = new ForkedMavenExecutor();

    protected abstract Executor doSelectExecutor();
}