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