package hudson.plugins.emma;
import hudson.FilePath;
import hudson.model.AbstractBuild;
import hudson.model.HealthReport;
import hudson.model.HealthReportingAction;
import hudson.model.Result;
import hudson.util.IOException2;
import hudson.util.NullStream;
import hudson.util.StreamTaskListener;
import org.jvnet.localizer.Localizable;
import org.kohsuke.stapler.StaplerProxy;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import hudson.plugins.emma.Messages;
/**
* Build view extension by Emma plugin.
*
* As {@link CoverageObject}, it retains the overall coverage report.
*
* @author Kohsuke Kawaguchi
*/
public final class EmmaBuildAction extends CoverageObject<EmmaBuildAction> implements HealthReportingAction, StaplerProxy {
public final AbstractBuild<?,?> owner;
private transient WeakReference<CoverageReport> report;
/**
* Non-null if the coverage has pass/fail rules.
*/
private final Rule rule;
/**
* The thresholds that applied when this build was built.
* @TODO add ability to trend thresholds on the graph
*/
private final EmmaHealthReportThresholds thresholds;
public EmmaBuildAction(AbstractBuild<?,?> owner, Rule rule, Ratio classCoverage, Ratio methodCoverage, Ratio blockCoverage, Ratio lineCoverage, EmmaHealthReportThresholds thresholds) {
this.owner = owner;
this.rule = rule;
this.clazz = classCoverage;
this.method = methodCoverage;
this.block = blockCoverage;
this.line = lineCoverage;
this.thresholds = thresholds;
}
public String getDisplayName() {
return Messages.BuildAction_DisplayName();
}
public String getIconFileName() {
return "graph.gif";
}
public String getUrlName() {
return "emma";
}
/**
* Get the coverage {@link hudson.model.HealthReport}.
*
* @return The health report or <code>null</code> if health reporting is disabled.
* @since 1.7
*/
public HealthReport getBuildHealth() {
if (thresholds == null) {
// no thresholds => no report
return null;
}
thresholds.ensureValid();
int score = 100, percent;
ArrayList<Localizable> reports = new ArrayList<Localizable>(5);
if (clazz != null && thresholds.getMaxClass() > 0) {
percent = clazz.getPercentage();
if (percent < thresholds.getMaxClass()) {
reports.add(Messages._BuildAction_Classes(clazz, percent));
}
score = updateHealthScore(score, thresholds.getMinClass(),
percent, thresholds.getMaxClass());
}
if (method != null && thresholds.getMaxMethod() > 0) {
percent = method.getPercentage();
if (percent < thresholds.getMaxMethod()) {
reports.add(Messages._BuildAction_Methods(method, percent));
}
score = updateHealthScore(score, thresholds.getMinMethod(),
percent, thresholds.getMaxMethod());
}
if (block != null && thresholds.getMaxBlock() > 0) {
percent = block.getPercentage();
if (percent < thresholds.getMaxBlock()) {
reports.add(Messages._BuildAction_Blocks(block, percent));
}
score = updateHealthScore(score, thresholds.getMinBlock(),
percent, thresholds.getMaxBlock());
}
if (line != null && thresholds.getMaxLine() > 0) {
percent = line.getPercentage();
if (percent < thresholds.getMaxLine()) {
reports.add(Messages._BuildAction_Lines(line, percent));
}
score = updateHealthScore(score, thresholds.getMinLine(),
percent, thresholds.getMaxLine());
}
if (score == 100) {
reports.add(Messages._BuildAction_Perfect());
}
// Collect params and replace nulls with empty string
Object[] args = reports.toArray(new Object[5]);
for (int i = 4; i >= 0; i--) if (args[i]==null) args[i] = ""; else break;
return new HealthReport(score, Messages._BuildAction_Description(
args[0], args[1], args[2], args[3], args[4]));
}
private static int updateHealthScore(int score, int min, int value, int max) {
if (value >= max) return score;
if (value <= min) return 0;
assert max != min;
final int scaled = (int) (100.0 * ((float) value - min) / (max - min));
if (scaled < score) return scaled;
return score;
}
public Object getTarget() {
return getResult();
}
@Override
public AbstractBuild<?,?> getBuild() {
return owner;
}
protected static FilePath[] getEmmaReports(File file) throws IOException, InterruptedException {
FilePath path = new FilePath(file);
if (path.isDirectory()) {
return path.list("*xml");
} else {
// Read old builds (before 1.11)
FilePath report = new FilePath(new File(path.getName() + ".xml"));
return report.exists() ? new FilePath[]{report} : new FilePath[0];
}
}
/**
* Obtains the detailed {@link CoverageReport} instance.
*/
public synchronized CoverageReport getResult() {
File reportFolder = EmmaPublisher.getEmmaReport(owner);
if(report!=null) {
CoverageReport r = report.get();
if(r!=null) return r;
}
try {
// Get the list of report files stored for this build
FilePath[] reports = getEmmaReports(reportFolder);
InputStream[] streams = new InputStream[reports.length];
for (int i=0; i<reports.length; i++) {
streams[i] = reports[i].read();
}
// Generate the report
CoverageReport r = new CoverageReport(this, streams);
if(rule!=null) {
// we change the report so that the FAILED flag is set correctly
logger.info("calculating failed packages based on " + rule);
rule.enforce(r,new StreamTaskListener(new NullStream()));
}
report = new WeakReference<CoverageReport>(r);
return r;
} catch (InterruptedException e) {
logger.log(Level.WARNING, "Failed to load " + reportFolder, e);
return null;
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to load " + reportFolder, e);
return null;
}
}
@Override
public EmmaBuildAction getPreviousResult() {
return getPreviousResult(owner);
}
/**
* Gets the previous {@link EmmaBuildAction} of the given build.
*/
/*package*/ static EmmaBuildAction getPreviousResult(AbstractBuild<?,?> start) {
AbstractBuild<?,?> b = start;
while(true) {
b = b.getPreviousBuild();
if(b==null)
return null;
if(b.getResult()== Result.FAILURE)
continue;
EmmaBuildAction r = b.getAction(EmmaBuildAction.class);
if(r!=null)
return r;
}
}
/**
* Constructs the object from emma XML report files.
* See <a href="http://emma.sourceforge.net/coverage_sample_c/coverage.xml">an example XML file</a>.
*
* @throws IOException
* if failed to parse the file.
*/
public static EmmaBuildAction load(AbstractBuild<?,?> owner, Rule rule, EmmaHealthReportThresholds thresholds, FilePath... files) throws IOException {
Ratio ratios[] = null;
for (FilePath f: files ) {
InputStream in = f.read();
try {
ratios = loadRatios(in, ratios);
} catch (XmlPullParserException e) {
throw new IOException2("Failed to parse " + f, e);
} finally {
in.close();
}
}
return new EmmaBuildAction(owner,rule,ratios[0],ratios[1],ratios[2],ratios[3],thresholds);
}
public static EmmaBuildAction load(AbstractBuild<?,?> owner, Rule rule, EmmaHealthReportThresholds thresholds, InputStream... streams) throws IOException, XmlPullParserException {
Ratio ratios[] = null;
for (InputStream in: streams) {
ratios = loadRatios(in, ratios);
}
return new EmmaBuildAction(owner,rule,ratios[0],ratios[1],ratios[2],ratios[3],thresholds);
}
private static Ratio[] loadRatios(InputStream in, Ratio[] r) throws IOException, XmlPullParserException {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser parser = factory.newPullParser();
parser.setInput(in,null);
while(true) {
if(parser.nextTag()!=XmlPullParser.START_TAG)
continue;
if(!parser.getName().equals("coverage"))
continue;
break;
}
if (r == null || r.length < 4)
r = new Ratio[4];
// head for the first <coverage> tag.
for( int i=0; i<r.length; i++ ) {
if(!parser.getName().equals("coverage"))
break; // line coverage is optional
parser.require(XmlPullParser.START_TAG,"","coverage");
String v = parser.getAttributeValue("", "value");
if (r[i] == null) {
r[i] = Ratio.parseValue(v);
} else {
r[i].addValue(v);
}
// move to the next coverage tag.
parser.nextTag();
parser.nextTag();
}
return r;
}
private static final Logger logger = Logger.getLogger(EmmaBuildAction.class.getName());
}