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

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

package com.yahoo.vespa.hosted.testrunner;

import com.google.inject.Inject;
import com.yahoo.vespa.defaults.Defaults;
import org.fusesource.jansi.AnsiOutputStream;
import org.fusesource.jansi.HtmlAnsiOutputStream;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.SortedMap;
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 com.yahoo.log.LogLevel.ERROR;

/**
 * @author valerijf
 * @author jvenstad
 */
public class TestRunner {

    private static final Logger logger = Logger.getLogger(TestRunner.class.getName());
    private static final Level HTML = new Level("html", 1) { };
    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 logFile;
    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("logs/vespa/maven.log"),
             vespaHome.resolve("tmp/config.json"),
             vespaHome.resolve("tmp/settings.xml"),
                     profile -> { // Anything to make this testable! >_<
                         String[] command = new String[]{
                                 "mvn",
                                 "test",

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

                                 // Disable maven download progress indication
                                 "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
                                 "-Dstyle.color=always", // Enable ANSI color codes again
                                 "-DfailIfNoTests=" + profile.failIfNoTests(),
                                 "-Dvespa.test.config=" + vespaHome.resolve("tmp/config.json"),
                                 "-Dvespa.test.credentials.root=" + Defaults.getDefaults().vespaHome() + "/var/vespa/sia",
                                 String.format("-DargLine=-Xms%1$dm -Xmx%1$dm", config.surefireMemoryMb())
                         };
                         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;
                     });
    }

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

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

        log.clear();
        status = Status.RUNNING;

        new Thread(() -> runTests(testProfile, testConfig)).start();
    }

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

    public synchronized Status getStatus() {
        return status;
    }

    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",
                                                           testProfile.name(), artifactsPath, configFile, String.join(" ", builder.command())));
            log.put(record.getSequenceNumber(), record);
            logger.log(record);
        }

        boolean success;
        // The AnsiOutputStream filters out ANSI characters, leaving the file contents pure.
        try (PrintStream fileStream = new PrintStream(new AnsiOutputStream(new BufferedOutputStream(new FileOutputStream(logFile.toFile()))));
             ByteArrayOutputStream logBuffer = new ByteArrayOutputStream();
             PrintStream logFormatter = new PrintStream(new HtmlAnsiOutputStream(logBuffer))){
            writeTestApplicationPom(testProfile);
            Files.write(configFile, testConfig);
            Files.write(settingsFile, settingsXml.getBytes());

            Process mavenProcess = builder.start();
            BufferedReader in = new BufferedReader(new InputStreamReader(mavenProcess.getInputStream()));
            in.lines().forEach(line -> {
                fileStream.println(line);
                logFormatter.print(line);
                LogRecord record = new LogRecord(HTML, logBuffer.toString());
                log.put(record.getSequenceNumber(), record);
                logBuffer.reset();
            });
            success = mavenProcess.waitFor() == 0;
        }
        catch (Exception exception) {
            LogRecord record = new LogRecord(ERROR, "Failed to execute maven command: " + String.join(" ", builder.command()));
            record.setThrown(exception);
            logger.log(record);
            log.put(record.getSequenceNumber(), record);
            try (PrintStream file = new PrintStream(new FileOutputStream(logFile.toFile(), true))) {
                file.println(record.getMessage());
                exception.printStackTrace(file);
            }
            catch (IOException ignored) { }
            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 IllegalStateException("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"))
                    .collect(Collectors.toList());
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to list files under " + directory, e);
        }
    }


    public enum Status {
        NOT_STARTED, RUNNING, FAILURE, ERROR, SUCCESS
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy