package hudson.plugins.japex;
import com.sun.japex.RegressionDetector;
import com.sun.japex.report.TestSuiteReport;
import hudson.Extension;
import hudson.Launcher;
import hudson.Util;
import hudson.FilePath;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Project;
import hudson.model.Result;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Mailer;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.FormValidation;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.StaplerRequest;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.QueryParameter;
/**
* Records the japex test report for builds.
*
* @author Kohsuke Kawaguchi
*/
public class JapexPublisher extends Recorder {
/**
* Relative path to the Japex XML report files.
*/
private String includes;
private boolean trackRegressions;
private double regressionThreshold;
private String regressionAddress;
public String getIncludes() {
return includes;
}
public void setIncludes(String includes) {
this.includes = includes;
}
public boolean isTrackRegressions() {
return trackRegressions;
}
public void setTrackRegressions(boolean trackRegressions) {
this.trackRegressions = trackRegressions;
}
public double getRegressionThreshold() {
return regressionThreshold;
}
public void setRegressionThreshold(double regressionThreshold) {
this.regressionThreshold = regressionThreshold;
}
public String getRegressionAddress() {
return regressionAddress;
}
public void setRegressionAddress(String regressionAddress) {
this.regressionAddress = Util.fixEmpty(Util.fixNull(regressionAddress).trim());
}
@Override
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
if (!(build instanceof Build)) return true;
listener.getLogger().println("Recording japex reports "+includes);
org.apache.tools.ant.Project antProject = new org.apache.tools.ant.Project();
File outDir = getJapexReport(build);
outDir.mkdir();
int numFiles = build.getWorkspace().copyRecursiveTo(includes, new FilePath(outDir));
if(numFiles==0) {
listener.error("No matching file found. Configuration error?");
build.setResult(Result.FAILURE);
return true;
}
// work with files copied to the local dir
FileSet fs = new FileSet();
fs.setDir(outDir);
DirectoryScanner ds = fs.getDirectoryScanner(antProject);
String[] includedFiles = ds.getIncludedFiles();
File prevDir = getPreviousJapexReport(build);
boolean hasRegressionReport = false;
for (String f : includedFiles) {
File file = new File(ds.getBasedir(),f);
if(file.lastModified()<build.getTimestamp().getTimeInMillis()) {
listener.getLogger().println("Ignoring old file: "+file);
continue;
}
listener.getLogger().println(file);
String configName;
try {
TestSuiteReport rpt = new TestSuiteReport(file);
configName = rpt.getParameters().get("configFile").replace('/','.');
} catch (Exception e) {
// TestSuiteReport ctor does throw RuntimeException
e.printStackTrace(listener.error(e.getMessage()));
continue;
}
// archive the report file
Util.copyFile(file,new File(outDir,configName));
// compute the regression
File previousConfig = new File(prevDir,configName);
if(previousConfig.exists()) {
try {
File regressionFile = new File(outDir, configName + ".regression");
RegressionDetector regd = new RegressionDetector();
regd.setOldReport(previousConfig);
regd.setNewReport(file);
regd.setThreshold(regressionThreshold);
regd.generateXmlReport(regressionFile);
hasRegressionReport = true;
if(trackRegressions && regd.checkThreshold(new StreamSource(regressionFile))) {
// regression detected
listener.getLogger().println("Regression detected to "+configName);
listener.getLogger().println("Notifying "+regressionAddress);
build.setResult(Result.UNSTABLE);
StringWriter html = new StringWriter();
regd.generateHtmlReport(new StreamSource(regressionFile),new StreamResult(html));
sendNotification(build,listener,html.toString());
}
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to compute japex regression report for "+configName));
}
}
}
if(hasRegressionReport)
build.getActions().add(new JapexReportBuildAction((Build)build)); // Type checked above
return true;
}
private void sendNotification(AbstractBuild<?,?> build, BuildListener listener, String payload) {
try {
Message msg = new MimeMessage(Mailer.descriptor().createSession());
msg.setRecipients(Message.RecipientType.TO,
InternetAddress.parse(getRegressionAddress(), false));
msg.setFrom(new InternetAddress(Mailer.descriptor().getAdminAddress()));
msg.setSubject("Japex performance regression in "+build.getProject().getFullDisplayName()+' '+build.getDisplayName());
msg.setText(payload);
msg.setHeader("Content-Type", "text/html");
Transport.send(msg);
} catch (MessagingException e) {
e.printStackTrace(listener.error("Failed to send out Japex notification e-mail"));
}
}
/**
* Computes the archive of the last Japex run.
*/
private File getPreviousJapexReport(AbstractBuild<?,?> build) {
build = build.getPreviousNotFailedBuild();
if(build==null) return null;
else return getJapexReport(build);
}
/**
* Gets the directory to store report files
*/
static File getJapexReport(AbstractBuild<?,?> build) {
return new File(build.getRootDir(),"japex");
}
@Override
public Action getProjectAction(AbstractProject<?,?> project) {
return project instanceof Project ? new JapexReportAction((Project)project) : null;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
@Override
public BuildStepDescriptor<Publisher> getDescriptor() {
return DESCRIPTOR;
}
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public DescriptorImpl() {
super(JapexPublisher.class);
}
public String getDisplayName() {
return "Record Japex test report";
}
@Override
public String getHelpFile() {
return "/plugin/japex/help.html";
}
@Override
public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException {
JapexPublisher pub = new JapexPublisher();
req.bindParameters(pub,"japex.");
if(pub.isTrackRegressions()) {
// make sure both the threshold and address are given
if(pub.getRegressionAddress()==null)
throw new FormException("No e-mail address is set","japex.trackRegressions");
try {
InternetAddress.parse(pub.getRegressionAddress(), false);
} catch (AddressException e) {
throw new FormException("Invalid e-mail format",e,"japex.trackRegressions");
}
}
return pub;
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return Project.class.isAssignableFrom(jobType);
}
//
// web methods
//
/**
* Checks if the e-mail address is valid
*/
public FormValidation doCheckAddress(@QueryParameter final String value) {
try {
InternetAddress.parse(value,true);
return FormValidation.ok();
} catch (AddressException e) {
return FormValidation.error("Not a valid e-mail address(es)");
}
}
}
}