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