package hudson.plugins.cobertura;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.*;
import hudson.plugins.cobertura.renderers.SourceCodePainter;
import hudson.plugins.cobertura.targets.CoverageMetric;
import hudson.plugins.cobertura.targets.CoverageResult;
import hudson.plugins.cobertura.targets.CoverageTarget;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import org.apache.commons.beanutils.ConvertUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.*;
import net.sf.json.JSONObject;
/**
* Cobertura {@link Publisher}.
*
* @author Stephen Connolly
*/
public class CoberturaPublisher extends Recorder {
private final String coberturaReportFile;
private final boolean onlyStable;
private CoverageTarget healthyTarget;
private CoverageTarget unhealthyTarget;
private CoverageTarget failingTarget;
public static final CoberturaReportFilenameFilter COBERTURA_FILENAME_FILTER = new CoberturaReportFilenameFilter();
/**
* @param coberturaReportFile the report directory
* @stapler-constructor
*/
@DataBoundConstructor
public CoberturaPublisher(String coberturaReportFile, boolean onlyStable) {
this.coberturaReportFile = coberturaReportFile;
this.onlyStable = onlyStable;
this.healthyTarget = new CoverageTarget();
this.unhealthyTarget = new CoverageTarget();
this.failingTarget = new CoverageTarget();
}
/**
* Getter for property 'targets'.
*
* @return Value for property 'targets'.
*/
public List<CoberturaPublisherTarget> getTargets() {
Map<CoverageMetric, CoberturaPublisherTarget> targets = new TreeMap<CoverageMetric, CoberturaPublisherTarget>();
for (CoverageMetric metric : healthyTarget.getTargets()) {
CoberturaPublisherTarget target = targets.get(metric);
if (target == null) {
target = new CoberturaPublisherTarget();
target.setMetric(metric);
}
target.setHealthy(healthyTarget.getTarget(metric));
targets.put(metric, target);
}
for (CoverageMetric metric : unhealthyTarget.getTargets()) {
CoberturaPublisherTarget target = targets.get(metric);
if (target == null) {
target = new CoberturaPublisherTarget();
target.setMetric(metric);
}
target.setUnhealthy(unhealthyTarget.getTarget(metric));
targets.put(metric, target);
}
for (CoverageMetric metric : failingTarget.getTargets()) {
CoberturaPublisherTarget target = targets.get(metric);
if (target == null) {
target = new CoberturaPublisherTarget();
target.setMetric(metric);
}
target.setUnstable(failingTarget.getTarget(metric));
targets.put(metric, target);
}
List<CoberturaPublisherTarget> result = new ArrayList<CoberturaPublisherTarget>(targets.values());
return result;
}
/**
* Setter for property 'targets'.
*
* @param targets Value to set for property 'targets'.
*/
private void setTargets(List<CoberturaPublisherTarget> targets) {
healthyTarget.clear();
unhealthyTarget.clear();
failingTarget.clear();
for (CoberturaPublisherTarget target : targets) {
if (target.getHealthy() != null) {
healthyTarget.setTarget(target.getMetric(), target.getHealthy());
}
if (target.getUnhealthy() != null) {
unhealthyTarget.setTarget(target.getMetric(), target.getUnhealthy());
}
if (target.getUnstable() != null) {
failingTarget.setTarget(target.getMetric(), target.getUnstable());
}
}
}
/**
* Getter for property 'coberturaReportFile'.
*
* @return Value for property 'coberturaReportFile'.
*/
public String getCoberturaReportFile() {
return coberturaReportFile;
}
/**
* Which type of build should be considered.
* @return the onlyStable
*/
public boolean getOnlyStable() {
return onlyStable;
}
/**
* Getter for property 'healthyTarget'.
*
* @return Value for property 'healthyTarget'.
*/
public CoverageTarget getHealthyTarget() {
return healthyTarget;
}
/**
* Setter for property 'healthyTarget'.
*
* @param healthyTarget Value to set for property 'healthyTarget'.
*/
public void setHealthyTarget(CoverageTarget healthyTarget) {
this.healthyTarget = healthyTarget;
}
/**
* Getter for property 'unhealthyTarget'.
*
* @return Value for property 'unhealthyTarget'.
*/
public CoverageTarget getUnhealthyTarget() {
return unhealthyTarget;
}
/**
* Setter for property 'unhealthyTarget'.
*
* @param unhealthyTarget Value to set for property 'unhealthyTarget'.
*/
public void setUnhealthyTarget(CoverageTarget unhealthyTarget) {
this.unhealthyTarget = unhealthyTarget;
}
/**
* Getter for property 'failingTarget'.
*
* @return Value for property 'failingTarget'.
*/
public CoverageTarget getFailingTarget() {
return failingTarget;
}
/**
* Setter for property 'failingTarget'.
*
* @param failingTarget Value to set for property 'failingTarget'.
*/
public void setFailingTarget(CoverageTarget failingTarget) {
this.failingTarget = failingTarget;
}
/**
* Gets the directory where the Cobertura Report is stored for the given project.
*/
/*package*/
static File getCoberturaReportDir(AbstractItem project) {
return new File(project.getRootDir(), "cobertura");
}
/**
* Gets the directory where the Cobertura Report is stored for the given project.
*/
/*package*/
static File[] getCoberturaReports(AbstractBuild<?,?> build) {
return build.getRootDir().listFiles(COBERTURA_FILENAME_FILTER);
}
/**
* {@inheritDoc}
*/
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
Result threshold = onlyStable ? Result.SUCCESS : Result.UNSTABLE;
if(build.getResult().isWorseThan(threshold)) {
listener.getLogger().println("Skipping Cobertura coverage report as build was not " + threshold.toString() + " or better ...");
return true;
}
listener.getLogger().println("Publishing Cobertura coverage report...");
final FilePath[] moduleRoots = build.getModuleRoots();
final boolean multipleModuleRoots =
moduleRoots != null && moduleRoots.length > 1;
final FilePath moduleRoot = multipleModuleRoots ? build.getWorkspace() : build.getModuleRoot();
final File buildCoberturaDir = build.getRootDir();
FilePath buildTarget = new FilePath(buildCoberturaDir);
FilePath[] reports = new FilePath[0];
try {
reports = moduleRoot.list(coberturaReportFile);
// if the build has failed, then there's not
// much point in reporting an error
if (build.getResult().isWorseOrEqualTo(Result.FAILURE) && reports.length == 0)
return true;
} catch (IOException e) {
Util.displayIOException(e, listener);
e.printStackTrace(listener.fatalError("Unable to find coverage results"));
build.setResult(Result.FAILURE);
}
if (reports.length == 0) {
String msg = "No coverage results were found using the pattern '"
+ coberturaReportFile + "' relative to '"
+ moduleRoot.getRemote() + "'."
+ " Did you enter a pattern relative to the correct directory?"
+ " Did you generate the XML report(s) for Cobertura?";
listener.getLogger().println(msg);
build.setResult(Result.FAILURE);
return true;
}
for (int i = 0; i < reports.length; i++) {
final FilePath targetPath = new FilePath(buildTarget, "coverage" + (i == 0 ? "" : i) + ".xml");
try {
reports[i].copyTo(targetPath);
} catch (IOException e) {
Util.displayIOException(e, listener);
e.printStackTrace(listener.fatalError("Unable to copy coverage from " + reports[i] + " to " + buildTarget));
build.setResult(Result.FAILURE);
}
}
listener.getLogger().println("Publishing Cobertura coverage results...");
Set<String> sourcePaths = new HashSet<String>();
CoverageResult result = null;
for (File coberturaXmlReport : getCoberturaReports(build)) {
try {
result = CoberturaCoverageParser.parse(coberturaXmlReport, result, sourcePaths);
} catch (IOException e) {
Util.displayIOException(e, listener);
e.printStackTrace(listener.fatalError("Unable to parse " + coberturaXmlReport));
build.setResult(Result.FAILURE);
}
}
if (result != null) {
result.setOwner(build);
final FilePath paintedSourcesPath = new FilePath(new File(build.getProject().getRootDir(), "cobertura"));
paintedSourcesPath.mkdirs();
SourceCodePainter painter = new SourceCodePainter(paintedSourcesPath, sourcePaths,
result.getPaintedSources(), listener);
moduleRoot.act(painter);
final CoberturaBuildAction action = CoberturaBuildAction.load(build, result, healthyTarget,
unhealthyTarget, getOnlyStable());
build.getActions().add(action);
Set<CoverageMetric> failingMetrics = failingTarget.getFailingMetrics(result);
if (!failingMetrics.isEmpty()) {
listener.getLogger().println("Code coverage enforcement failed for the following metrics:");
for (CoverageMetric metric : failingMetrics) {
listener.getLogger().println(" " + metric.getName());
}
listener.getLogger().println("Setting Build to unstable.");
build.setResult(Result.UNSTABLE);
}
} else {
listener.getLogger().println("No coverage results were successfully parsed. Did you generate " +
"the XML report(s) for Cobertura?");
build.setResult(Result.FAILURE);
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public Action getProjectAction(AbstractProject<?, ?> project) {
return new CoberturaProjectAction(project, getOnlyStable());
}
/**
* {@inheritDoc}
*/
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
/**
* {@inheritDoc}
*/
@Override
public BuildStepDescriptor<Publisher> getDescriptor() {
// see Descriptor javadoc for more about what a descriptor is.
return DESCRIPTOR;
}
/**
* Descriptor should be singleton.
*/
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
/**
* Descriptor for {@link CoberturaPublisher}. Used as a singleton. The class is marked as public so that it can be
* accessed from views.
* <p/>
* <p/>
* See <tt>views/hudson/plugins/cobertura/CoberturaPublisher/*.jelly</tt> for the actual HTML fragment for the
* configuration screen.
*/
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
CoverageMetric[] metrics = {
CoverageMetric.PACKAGES,
CoverageMetric.FILES,
CoverageMetric.CLASSES,
CoverageMetric.METHOD,
CoverageMetric.LINE,
CoverageMetric.CONDITIONAL,
};
/**
* Constructs a new DescriptorImpl.
*/
DescriptorImpl() {
super(CoberturaPublisher.class);
}
/**
* This human readable name is used in the configuration screen.
*/
public String getDisplayName() {
return Messages.CoberturaPublisher_displayName();
}
/**
* Getter for property 'metrics'.
*
* @return Value for property 'metrics'.
*/
public List<CoverageMetric> getMetrics() {
return Arrays.asList(metrics);
}
/**
* Getter for property 'defaultTargets'.
*
* @return Value for property 'defaultTargets'.
*/
public List<CoberturaPublisherTarget> getDefaultTargets() {
List<CoberturaPublisherTarget> result = new ArrayList<CoberturaPublisherTarget>();
result.add(new CoberturaPublisherTarget(CoverageMetric.METHOD, 80, null, null));
result.add(new CoberturaPublisherTarget(CoverageMetric.LINE, 80, null, null));
result.add(new CoberturaPublisherTarget(CoverageMetric.CONDITIONAL, 70, null, null));
return result;
}
public List<CoberturaPublisherTarget> getTargets(CoberturaPublisher instance) {
if (instance == null) {
return getDefaultTargets();
}
return instance.getTargets();
}
/**
* {@inheritDoc}
*/
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
req.bindParameters(this, "cobertura.");
save();
return super.configure(req, formData);
}
/**
* Creates a new instance of {@link CoberturaPublisher} from a submitted form.
*/
@Override
public CoberturaPublisher newInstance(StaplerRequest req, JSONObject formData) throws FormException {
CoberturaPublisher instance = req.bindParameters(CoberturaPublisher.class, "cobertura.");
ConvertUtils.register(CoberturaPublisherTarget.CONVERTER, CoverageMetric.class);
List<CoberturaPublisherTarget> targets = req
.bindParametersToList(CoberturaPublisherTarget.class, "cobertura.target.");
instance.setTargets(targets);
return instance;
}
@SuppressWarnings("unchecked")
@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
}
private static class CoberturaReportFilenameFilter implements FilenameFilter {
/**
* {@inheritDoc}
*/
public boolean accept(File dir, String name) {
// TODO take this out of an anonymous inner class, create a singleton and use a Regex to match the name
return name.startsWith("coverage") && name.endsWith(".xml");
}
}
}