All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.vespa.hosted.testrunner.TestRunner Maven / Gradle / Ivy

The newest version!
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.testrunner;

import com.yahoo.component.annotation.Inject;
import com.yahoo.vespa.defaults.Defaults;
import com.yahoo.vespa.testrunner.HtmlLogger;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.logging.Level.SEVERE;

/**
 * @author valerijf
 * @author jvenstad
 */
public class TestRunner implements com.yahoo.vespa.testrunner.TestRunner {

    private static final Logger logger = Logger.getLogger(TestRunner.class.getName());
    private static final Path vespaHome = Paths.get(Defaults.getDefaults().vespaHome());
    private static final String settingsXml = "\n" +
                                              "\n" +
                                              "    \n" +
                                              "        \n" +
                                              "            maven central\n" +
                                              "            *\n" + // Use this for everything!
                                              "            https://repo.maven.apache.org/maven2/\n" +
                                              "        \n" +
                                              "    \n" +
                                              "";

    private final Path artifactsPath;
    private final Path testPath;
    private final Path configFile;
    private final Path settingsFile;
    private final Function testBuilder;
    private final SortedMap log = new ConcurrentSkipListMap<>();

    private volatile Status status = Status.NOT_STARTED;

    @Inject
    public TestRunner(TestRunnerConfig config) {
        this(config.artifactsPath(),
             vespaHome.resolve("tmp/test"),
             vespaHome.resolve("tmp/config.json"),
             vespaHome.resolve("tmp/settings.xml"),
             profile -> mavenProcessFrom(profile, config));
    }

    TestRunner(Path artifactsPath, Path testPath, Path configFile, Path settingsFile, Function testBuilder) {
        this.artifactsPath = artifactsPath;
        this.testPath = testPath;
        this.configFile = configFile;
        this.settingsFile = settingsFile;
        this.testBuilder = testBuilder;
    }

    static ProcessBuilder mavenProcessFrom(TestProfile profile, TestRunnerConfig config) {
        List command = new ArrayList<>();
        command.add("mvn"); // mvn must be in PATH of the jDisc containers
        command.add("test");

        command.add("--batch-mode"); // Run in non-interactive (batch) mode (disables output color)
        command.add("--show-version"); // Display version information WITHOUT stopping build
        command.add("--settings"); // Need to override repository settings in ymaven config >_<
        command.add(vespaHome.resolve("tmp/settings.xml").toString());

        // Disable maven download progress indication
        command.add("-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn");
        command.add("-Dstyle.color=always"); // Enable ANSI color codes again
        command.add("-DfailIfNoTests=" + profile.failIfNoTests());
        command.add("-Dvespa.test.config=" + vespaHome.resolve("tmp/config.json"));
        if (config.useAthenzCredentials())
            command.add("-Dvespa.test.credentials.root=" + Defaults.getDefaults().underVespaHome("var/vespa/sia"));
        else if (config.useTesterCertificate())
            command.add("-Dvespa.test.credentials.root=" + config.artifactsPath());
        command.add(String.format("-DargLine=-Xms%1$dm -Xmx%1$dm", config.surefireMemoryMb()));
        command.add("-Dmaven.repo.local=" + vespaHome.resolve("tmp/.m2/repository"));

        ProcessBuilder builder = new ProcessBuilder(command);
        builder.environment().merge("MAVEN_OPTS", " -Djansi.force=true", String::concat);
        builder.directory(vespaHome.resolve("tmp/test").toFile());
        builder.redirectErrorStream(true);
        return builder;
    }

    @Override
    public synchronized CompletableFuture test(Suite suite, byte[] testConfig) {
        if (status == Status.RUNNING)
            throw new IllegalArgumentException("Tests are already running; should not receive this request now.");

        log.clear();

        if ( ! hasTestsJar()) {
            status = Status.NO_TESTS;
            return CompletableFuture.completedFuture(null);
        }

        status = Status.RUNNING;
        return CompletableFuture.runAsync(() -> runTests(toProfile(suite), testConfig));
    }

    @Override
    public Collection getLog(long after) {
        return log.tailMap(after + 1).values();
    }

    @Override
    public synchronized Status getStatus() {
        return status;
    }

    private boolean hasTestsJar() {
        return listFiles(artifactsPath).stream().anyMatch(file -> file.toString().endsWith("tests.jar"));
    }

    private void runTests(TestProfile testProfile, byte[] testConfig) {
        ProcessBuilder builder = testBuilder.apply(testProfile);
        {
            LogRecord record = new LogRecord(Level.INFO,
                                             String.format("Starting %s. Artifacts directory: %s Config file: %s\nCommand to run: %s\nEnv: %s\n",
                                                           testProfile.name(), artifactsPath, configFile,
                                                           String.join(" ", builder.command()),
                                                           builder.environment()));
            log.put(record.getSequenceNumber(), record);
            logger.log(record);
            log.put(record.getSequenceNumber(), record);
            logger.log(record);
        }

        boolean success;
        try {
            writeTestApplicationPom(testProfile);
            Files.write(configFile, testConfig);
            Files.write(settingsFile, settingsXml.getBytes());

            Process mavenProcess = builder.start();
            BufferedReader in = new BufferedReader(new InputStreamReader(mavenProcess.getInputStream()));
            HtmlLogger htmlLogger = new HtmlLogger();
            in.lines().forEach(line -> {
                LogRecord html = htmlLogger.toLog(line);
                log.put(html.getSequenceNumber(), html);
            });
            success = mavenProcess.waitFor() == 0;
        }
        catch (Exception exception) {
            LogRecord record = new LogRecord(SEVERE, "Failed to execute maven command: " + String.join(" ", builder.command()));
            record.setThrown(exception);
            logger.log(record);
            log.put(record.getSequenceNumber(), record);
            status = Status.ERROR;
            return;
        }
        status = success ? Status.SUCCESS : Status.FAILURE;
    }

    private void writeTestApplicationPom(TestProfile testProfile) throws IOException {
        List files = listFiles(artifactsPath);
        Path testJar = files.stream().filter(file -> file.toString().endsWith("tests.jar")).findFirst()
                       .orElseThrow(() -> new NoTestsException("No file ending with 'tests.jar' found under '" + artifactsPath + "'!"));
        String pomXml = PomXmlGenerator.generatePomXml(testProfile, files, testJar);
        testPath.toFile().mkdirs();
        Files.write(testPath.resolve("pom.xml"), pomXml.getBytes());
    }

    private static List listFiles(Path directory) {
        try (Stream element = Files.walk(directory)) {
            return element
                    .filter(Files::isRegularFile)
                    .filter(path -> path.toString().endsWith(".jar"))
                    .toList();
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to list files under " + directory, e);
        }
    }


    static class NoTestsException extends RuntimeException {
        private NoTestsException(String message) { super(message); }
    }

    static TestProfile toProfile(Suite suite) {
        switch (suite) {
            case SYSTEM_TEST: return TestProfile.SYSTEM_TEST;
            case STAGING_SETUP_TEST: return TestProfile.STAGING_SETUP_TEST;
            case STAGING_TEST: return TestProfile.STAGING_TEST;
            case PRODUCTION_TEST: return TestProfile.PRODUCTION_TEST;
            default: throw new IllegalArgumentException("Unknown test suite '" + suite + "'");
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy