/*
* Copyright (c) 2012-2013, CloudBees, Inc., SOASTA, Inc.
* All Rights Reserved.
*/
package com.soasta.jenkins;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Launcher.LocalLauncher;
import hudson.model.AutoCompletionCandidates;
import hudson.model.BuildListener;
import hudson.model.Saveable;
import hudson.model.TaskListener;
import hudson.model.AbstractBuild;
import hudson.model.Descriptor;
import hudson.tasks.junit.TestDataPublisher;
import hudson.tasks.junit.JUnitResultArchiver;
import hudson.util.ArgumentListBuilder;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import hudson.util.QuotedStringTokenizer;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringReader;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
/**
* @author Kohsuke Kawaguchi
*/
public class TestCompositionRunner extends AbstractSCommandBuilder {
/**
* Composition to execute.
*/
private final String composition;
private final boolean deleteOldResults;
private final int maxDaysOfResults;
private final String additionalOptions;
private final List<TransactionThreshold> thresholds;
private final boolean generatePlotCSV;
@DataBoundConstructor
public TestCompositionRunner(String url, String cloudTestServerID, String composition, DeleteOldResultsSettings deleteOldResults,
String additionalOptions, List<TransactionThreshold> thresholds, boolean generatePlotCSV) {
super(url, cloudTestServerID);
this.composition = composition;
this.deleteOldResults = (deleteOldResults != null);
this.maxDaysOfResults = (deleteOldResults == null ? 0 : deleteOldResults.maxDaysOfResults);
this.additionalOptions = additionalOptions;
this.thresholds = thresholds;
this.generatePlotCSV = generatePlotCSV;
}
public List<TransactionThreshold> getThresholds() {
return thresholds;
}
public String getComposition() {
return composition;
}
public boolean getDeleteOldResults() {
return deleteOldResults;
}
public int getMaxDaysOfResults() {
return maxDaysOfResults;
}
public String getAdditionalOptions() {
return additionalOptions;
}
public boolean getGeneratePlotCSV() {
return generatePlotCSV;
}
public Object readResolve() throws IOException {
if (getCloudTestServerID() != null)
return this;
// We don't have a server ID.
// This means the builder config is based an older version the plug-in.
// Look up the server by URL instead.
// We'll use the ID going forward.
CloudTestServer s = CloudTestServer.getByURL(getUrl());
LOGGER.info("Matched server URL " + getUrl() + " to ID: " + s.getId() + "; re-creating.");
return new TestCompositionRunner(getUrl(), s.getId(), composition, deleteOldResults ? new DeleteOldResultsSettings(maxDaysOfResults) : null, additionalOptions, thresholds, generatePlotCSV);
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
// Create a unique sub-directory to store all test results.
String resultsDir = "." + getClass().getName();
// Split by newline.
EnvVars envs = build.getEnvironment(listener);
String[] compositions = envs.expand(this.composition).split("[\r\n]+");
String additionalOptionsExpanded = additionalOptions == null ?
null : envs.expand(additionalOptions);
String[] options = additionalOptionsExpanded == null ?
null : new QuotedStringTokenizer(additionalOptionsExpanded).toArray();
for (String composition : compositions) {
ArgumentListBuilder args = getSCommandArgs(build, listener);
args.add("cmd=play", "wait", "format=junitxml")
.add("name=" + composition);
// if thresholds are included in this post-build action, add them to scommand arguments
if (thresholds != null) {
displayTransactionThreholds(listener.getLogger());
for (TransactionThreshold threshold : thresholds) {
args.add("validation=" + threshold.toScommandString());
}
}
String fileName = composition + ".xml";
// Strip off any leading slash characters (composition names
// will typically be the full CloudTest folder path).
if (fileName.startsWith("/")) {
fileName = fileName.substring(1);
}
// Put the file in the test results directory.
fileName = resultsDir + File.separator + fileName;
FilePath xml = new FilePath(build.getWorkspace(), fileName);
// Make sure the directory exists.
xml.getParent().mkdirs();
// Add the additional options to the composition if there are any.
if (options != null) {
args.add(options);
}
if (generatePlotCSV) {
args.add("outputthresholdcsvdir=" + build.getWorkspace());
}
// Run it!
int exitCode = launcher
.launch()
.cmds(args)
.pwd(build.getWorkspace())
.stdout(xml.write())
.stderr(listener.getLogger())
.join();
if (xml.length() == 0) {
// SCommand did not produce any output.
// This should never happen, but just in case...
return false;
}
if (deleteOldResults) {
// Run SCommand again to clean up the old results.
args = getSCommandArgs(build, listener);
args.add("cmd=delete", "type=result")
.add("path=" + composition)
.add("maxage=" + maxDaysOfResults);
launcher
.launch()
.cmds(args)
.pwd(build.getWorkspace())
.stdout(listener)
.stderr(listener.getLogger())
.join();
}
}
// Now that we've finished running all the compositions, pass
// the results directory off to the JUnit archiver.
String resultsPattern = resultsDir + "/**/*.xml";
JUnitResultArchiver archiver = new JUnitResultArchiver(
resultsPattern,
true,
new DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>>(
Saveable.NOOP,
Collections.singleton(new JunitResultPublisher(null))));
return archiver.perform(build,launcher,listener);
}
private void displayTransactionThreholds(PrintStream jenkinsLogger) {
String THRESHOLD_TABLE_FORMAT = "%-15s %-20s %7s";
jenkinsLogger.println("~");
jenkinsLogger.println("Custom Transaction Threholds:");
for (TransactionThreshold threshold : thresholds) {
String formattedString = String.format(THRESHOLD_TABLE_FORMAT,threshold.getTransactionname(),threshold.getThresholdname(),threshold.getThresholdvalue());
jenkinsLogger.println(formattedString);
}
jenkinsLogger.println("~");
}
@Extension
public static class DescriptorImpl extends AbstractCloudTestBuilderDescriptor {
@Override
public String getDisplayName() {
return "Play Composition(s)";
}
/**
* Called automatically by Jenkins whenever the "composition"
* field is modified by the user.
* @param value the new composition name.
*/
public FormValidation doCheckComposition(@QueryParameter String value) {
if (value == null || value.trim().isEmpty()) {
return FormValidation.error("Composition name is required.");
} else {
return FormValidation.ok();
}
}
/**
* Called automatically by Jenkins whenever the "maxDaysOfResults"
* field is modified by the user.
* @param value the new maximum age, in days.
*/
public FormValidation doCheckMaxDaysOfResults(@QueryParameter String value) {
if (value == null || value.trim().isEmpty()) {
return FormValidation.error("Days to keep results is required.");
} else {
try {
int maxDays = Integer.parseInt(value);
if (maxDays <= 0) {
return FormValidation.error("Value must be > 0.");
} else {
return FormValidation.ok();
}
} catch (NumberFormatException e) {
return FormValidation.error("Value must be numeric.");
}
}
}
public AutoCompletionCandidates doAutoCompleteComposition(@QueryParameter String cloudTestServerID) throws IOException, InterruptedException {
CloudTestServer s = CloudTestServer.getByID(cloudTestServerID);
ArgumentListBuilder args = new ArgumentListBuilder();
args.add(install(s))
.add("list", "type=composition")
.add("url=" + s.getUrl())
.add("username=" + s.getUsername());
if (s.getPassword() != null)
args.addMasked("password=" + s.getPassword());
ByteArrayOutputStream out = new ByteArrayOutputStream();
int exit = new LocalLauncher(TaskListener.NULL).launch().cmds(args).stdout(out).join();
if (exit==0) {
BufferedReader r = new BufferedReader(new StringReader(out.toString()));
AutoCompletionCandidates a = new AutoCompletionCandidates();
String line;
while ((line=r.readLine())!=null) {
if (line.endsWith("object(s) found.")) continue;
a.add(line);
}
return a;
}
return new AutoCompletionCandidates(); // no candidate
}
private synchronized FilePath install(CloudTestServer s) throws IOException, InterruptedException {
SCommandInstaller sCommandInstaller = new SCommandInstaller(s);
return sCommandInstaller.scommand(Jenkins.getInstance(), TaskListener.NULL);
}
}
public static class DeleteOldResultsSettings {
private final int maxDaysOfResults;
@DataBoundConstructor
public DeleteOldResultsSettings(int maxDaysOfResults) {
this.maxDaysOfResults = maxDaysOfResults;
}
public int getMaxDaysOfResults() {
return maxDaysOfResults;
}
}
private static final Logger LOGGER = Logger.getLogger(TestCompositionRunner.class.getName());
}