package hudson.plugins.emma;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
/**
* {@link Publisher} that captures Emma coverage reports.
*
* @author Kohsuke Kawaguchi
*/
public class EmmaPublisher extends Recorder {
/**
* Relative path to the Emma XML file inside the workspace.
*/
public String includes;
/**
* Rule to be enforced. Can be null.
*
* TODO: define a configuration mechanism.
*/
public Rule rule;
/**
* {@link hudson.model.HealthReport} thresholds to apply.
*/
public EmmaHealthReportThresholds healthReports = new EmmaHealthReportThresholds();
/**
* look for emma reports based in the configured parameter includes.
* 'includes' is
* - an Ant-style pattern
* - a list of files and folders separated by the characters ;:,
*/
protected static FilePath[] locateCoverageReports(FilePath workspace, String includes) throws IOException, InterruptedException {
// First use ant-style pattern
try {
FilePath[] ret = workspace.list(includes);
if (ret.length > 0) {
return ret;
}
} catch (Exception e) {
}
// If it fails, do a legacy search
ArrayList<FilePath> files = new ArrayList<FilePath>();
String parts[] = includes.split("\\s*[;:,]+\\s*");
for (String path : parts) {
FilePath src = workspace.child(path);
if (src.exists()) {
if (src.isDirectory()) {
files.addAll(Arrays.asList(src.list("**/coverage*.xml")));
} else {
files.add(src);
}
}
}
return files.toArray(new FilePath[files.size()]);
}
/**
* save emma reports from the workspace to build folder
*/
protected static void saveCoverageReports(FilePath folder, FilePath[] files) throws IOException, InterruptedException {
folder.mkdirs();
for (int i = 0; i < files.length; i++) {
String name = "coverage" + (i > 0 ? i : "") + ".xml";
FilePath src = files[i];
FilePath dst = folder.child(name);
src.copyTo(dst);
}
}
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
final PrintStream logger = listener.getLogger();
FilePath[] reports;
if (includes == null || includes.trim().length() == 0) {
logger.println("Emma: looking for coverage reports in the entire workspace: " + build.getWorkspace().getRemote());
reports = locateCoverageReports(build.getWorkspace(), "**/emma/coverage.xml");
} else {
logger.println("Emma: looking for coverage reports in the provided path: " + includes );
reports = locateCoverageReports(build.getWorkspace(), includes);
}
if (reports.length == 0) {
if(build.getResult().isWorseThan(Result.UNSTABLE))
return true;
logger.println("Emma: no coverage files found in workspace. Was any report generated?");
build.setResult(Result.FAILURE);
return true;
} else {
String found = "";
for (FilePath f: reports)
found += "\n " + f.getRemote();
logger.println("Emma: found " + reports.length + " report files: " + found );
}
FilePath emmafolder = new FilePath(getEmmaReport(build));
saveCoverageReports(emmafolder, reports);
logger.println("Emma: stored " + reports.length + " report files in the build folder: "+ emmafolder);
final EmmaBuildAction action = EmmaBuildAction.load(build, rule, healthReports, reports);
logger.println("Emma: " + action.getBuildHealth().getDescription());
build.getActions().add(action);
if (action.getResult().isFailed()) {
logger.println("Emma: code coverage enforcement failed. Setting Build to unstable.");
build.setResult(Result.UNSTABLE);
}
return true;
}
@Override
public Action getProjectAction(AbstractProject<?, ?> project) {
return new EmmaProjectAction(project);
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
/**
* Gets the directory to store report files
*/
static File getEmmaReport(AbstractBuild<?,?> build) {
return new File(build.getRootDir(), "emma");
}
@Override
public BuildStepDescriptor<Publisher> getDescriptor() {
return DESCRIPTOR;
}
@Extension
public static final BuildStepDescriptor<Publisher> DESCRIPTOR = new DescriptorImpl();
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public DescriptorImpl() {
super(EmmaPublisher.class);
}
public String getDisplayName() {
return Messages.EmmaPublisher_DisplayName();
}
@Override
public String getHelpFile() {
return "/plugin/emma/help.html";
}
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
@Override
public Publisher newInstance(StaplerRequest req, JSONObject json) throws FormException {
EmmaPublisher pub = new EmmaPublisher();
req.bindParameters(pub, "emma.");
req.bindParameters(pub.healthReports, "emmaHealthReports.");
// start ugly hack
//@TODO remove ugly hack
// the default converter for integer values used by req.bindParameters
// defaults an empty value to 0. This happens even if the type is Integer
// and not int. We want to change the default values, so we use this hack.
//
// If you know a better way, please fix.
if ("".equals(req.getParameter("emmaHealthReports.maxClass"))) {
pub.healthReports.setMaxClass(100);
}
if ("".equals(req.getParameter("emmaHealthReports.maxMethod"))) {
pub.healthReports.setMaxMethod(70);
}
if ("".equals(req.getParameter("emmaHealthReports.maxBlock"))) {
pub.healthReports.setMaxBlock(80);
}
if ("".equals(req.getParameter("emmaHealthReports.maxLine"))) {
pub.healthReports.setMaxLine(80);
}
// end ugly hack
return pub;
}
}
}