Skip to content

Commit

Permalink
[JENKINS-51364] Adopt Threshold conditions (e.g. from Cobertura) so t…
Browse files Browse the repository at this point in the history
…hat the reporter can fail the build depending on conditions.

- add global threshold for all coverage report.
- add local threshold for each coverage report
- add 'fail if unhealthy' option
- add health report
  • Loading branch information
cizezsy committed May 28, 2018
1 parent ea04c38 commit bf05960
Show file tree
Hide file tree
Showing 14 changed files with 335 additions and 151 deletions.
28 changes: 20 additions & 8 deletions src/main/java/io/jenkins/plugins/coverage/CoverageAction.java
@@ -1,25 +1,27 @@
package io.jenkins.plugins.coverage;

import hudson.model.Action;
import hudson.model.HealthReport;
import hudson.model.HealthReportingAction;
import hudson.model.Run;
import hudson.model.*;
import io.jenkins.plugins.coverage.targets.CoverageResult;
import io.jenkins.plugins.coverage.targets.Ratio;
import io.jenkins.plugins.coverage.threshold.Threshold;
import jenkins.model.RunAction2;
import jenkins.tasks.SimpleBuildStep;
import org.jvnet.localizer.Localizable;
import org.kohsuke.stapler.StaplerProxy;

import javax.annotation.CheckForNull;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class CoverageAction implements StaplerProxy, SimpleBuildStep.LastBuildAction, RunAction2, HealthReportingAction {

private transient Run<?, ?> owner;
private transient WeakReference<CoverageResult> report;

private HealthReport healthReport;

public CoverageAction(CoverageResult result) {
this.report = new WeakReference<>(result);
Expand All @@ -35,9 +37,12 @@ public Collection<? extends Action> getProjectActions() {
}


/**
* @return Health report
*/
@Override
public HealthReport getBuildHealth() {
return null;
return getHealthReport();
}

/**
Expand All @@ -55,7 +60,7 @@ private CoverageResult getResult() {

CoverageResult r = null;
try {
r = CoverageProcessor.recoverReport(owner);
r = CoverageProcessor.recoverCoverageResult(owner);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
Expand All @@ -76,6 +81,14 @@ public Object getTarget() {
}


public HealthReport getHealthReport() {
return healthReport;
}

public void setHealthReport(HealthReport healthReport) {
this.healthReport = healthReport;
}

private synchronized void setOwner(Run<?, ?> owner) {
this.owner = owner;
if (report != null) {
Expand All @@ -91,7 +104,6 @@ private synchronized void setOwner(Run<?, ?> owner) {
return owner;
}


/**
* {@inheritDoc}
*/
Expand Down
161 changes: 135 additions & 26 deletions src/main/java/io/jenkins/plugins/coverage/CoverageProcessor.java
Expand Up @@ -4,24 +4,30 @@
import com.google.common.collect.Sets;
import hudson.DescriptorExtensionList;
import hudson.FilePath;
import hudson.model.HealthReport;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import io.jenkins.plugins.coverage.adapter.CoverageReportAdapter;
import io.jenkins.plugins.coverage.adapter.CoverageReportAdapterDescriptor;
import io.jenkins.plugins.coverage.adapter.Detectable;
import io.jenkins.plugins.coverage.exception.ConversionException;
import io.jenkins.plugins.coverage.exception.CoverageException;
import io.jenkins.plugins.coverage.targets.CoverageElement;
import io.jenkins.plugins.coverage.targets.CoverageResult;
import io.jenkins.plugins.coverage.targets.Ratio;
import io.jenkins.plugins.coverage.threshold.Threshold;
import jenkins.MasterToSlaveFileCallable;
import org.apache.commons.io.FileUtils;
import org.jvnet.localizer.Localizable;

import javax.annotation.Nonnull;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* @author Shenyu Zheng
Expand All @@ -33,30 +39,31 @@ public class CoverageProcessor {
private Run<?, ?> run;
private FilePath workspace;
private TaskListener listener;
private List<CoverageReportAdapter> adapters;
private List<Threshold> globalThresholds;


private boolean enableAutoDetect;
private String autoDetectPath;

public CoverageProcessor(@Nonnull Run<?, ?> run, @Nonnull FilePath workspace, @Nonnull TaskListener listener,
List<CoverageReportAdapter> adapters, List<Threshold> globalThresholds) {
public CoverageProcessor(@Nonnull Run<?, ?> run, @Nonnull FilePath workspace, @Nonnull TaskListener listener) {
this.run = run;
this.workspace = workspace;
this.listener = listener;
this.adapters = adapters;
this.globalThresholds = globalThresholds;
}


public CoverageResult processCoverageReport() throws IOException, InterruptedException {
List<CoverageResult> results = convertToResults(adapters);
CoverageResult report = aggregatedToReport(results);
public void performCoverageReport(List<CoverageReportAdapter> adapters, List<Threshold> globalThresholds) throws IOException, InterruptedException, CoverageException {
Map<CoverageReportAdapter, List<CoverageResult>> results = convertToResults(adapters);
CoverageResult coverageReport = aggregatedResults(results);

saveReport(run, report);
coverageReport.setOwner(run);
HealthReport healthReport = processThresholds(results, globalThresholds);

saveCoverageResult(run, coverageReport);

CoverageAction action = new CoverageAction(coverageReport);
action.setHealthReport(healthReport);
run.addAction(action);

return report;
}

/**
Expand All @@ -65,7 +72,7 @@ public CoverageResult processCoverageReport() throws IOException, InterruptedExc
* @param adapters {@link CoverageReportAdapter} for each report
* @return {@link CoverageResult} for each report
*/
private List<CoverageResult> convertToResults(List<CoverageReportAdapter> adapters)
private Map<CoverageReportAdapter, List<CoverageResult>> convertToResults(List<CoverageReportAdapter> adapters)
throws IOException, InterruptedException {

Map<CoverageReportAdapter, Set<FilePath>> reports = new HashMap<>();
Expand Down Expand Up @@ -141,13 +148,14 @@ private List<CoverageResult> convertToResults(List<CoverageReportAdapter> adapte
reports.clear();


List<CoverageResult> results = new LinkedList<>();
Map<CoverageReportAdapter, List<CoverageResult>> results = new HashMap<>();
for (Map.Entry<CoverageReportAdapter, List<File>> adapterReports : copiedReport.entrySet()) {
CoverageReportAdapter adapter = adapterReports.getKey();
for (File s : adapterReports.getValue()) {
try {
results.add(adapter.getResult(s));
} catch (ConversionException e) {
results.putIfAbsent(adapter, new LinkedList<>());
results.get(adapter).add(adapter.getResult(s));
} catch (CoverageException e) {
e.printStackTrace();
listener.getLogger().printf("report for %s has met some errors: %s",
adapter.getDescriptor().getDisplayName(), e.getMessage());
Expand All @@ -158,6 +166,87 @@ private List<CoverageResult> convertToResults(List<CoverageReportAdapter> adapte
return results;
}

/**
* Process threshold
*
* @param adapterWithResults Coverage report adapter and its correspond Coverage results.
* @param globalThresholds global threshold
* @return Health report
*/
private HealthReport processThresholds(Map<CoverageReportAdapter, List<CoverageResult>> adapterWithResults,
List<Threshold> globalThresholds) throws CoverageException {

int healthy = 0;
int unhealthy = 0;

LinkedList<CoverageResult> resultTask = new LinkedList<>();

for (Map.Entry<CoverageReportAdapter, List<CoverageResult>> results : adapterWithResults.entrySet()) {

// make local threshold over global threshold
List<Threshold> thresholds = results.getKey().getThresholds();
if (thresholds != null) {
for (Threshold t : globalThresholds) {
if (!thresholds.contains(t)) {
thresholds.add(t);
}
}
} else {
thresholds = globalThresholds;
}

for (CoverageResult coverageResult : results.getValue()) {
resultTask.push(coverageResult);

while (!resultTask.isEmpty()) {
CoverageResult r = resultTask.pollFirst();
assert r != null;

resultTask.addAll(r.getChildrenReal().values());
for (Threshold threshold : thresholds) {
Ratio ratio = r.getCoverage(threshold.getThreshTarget());
if (ratio == null) {
continue;
}

float percentage = ratio.getPercentageFloat();
if (percentage < threshold.getUnhealthyThresh()) {
if (threshold.isFailUnhealthy()) {
throw new CoverageException(String.format("Publish Coverage Failed: %s coverage in %s is lower than %.2f",
threshold.getThreshTarget().getName(),
r.getName(), threshold.getUnhealthyThresh()));
} else {
unhealthy++;
}
} else {
healthy++;
}

if (percentage < threshold.getUnstableThresh()) {
run.setResult(Result.UNSTABLE);
}
}
}
}
}


resultTask.addAll(
adapterWithResults.values().stream()
.flatMap((Function<List<CoverageResult>, Stream<CoverageResult>>) Collection::stream)
.collect(Collectors.toList()));

int score;
if (healthy == 0 && unhealthy == 0) {
score = 100;
} else {
score = healthy * 100 / (healthy + unhealthy);
}
Localizable localizeDescription = Messages._CoverageProcessor_healthReportDescriptionTemplate(score);

return new HealthReport(score, localizeDescription);
}

/**
* Find all file that can match report adapter
*
Expand Down Expand Up @@ -201,10 +290,18 @@ private Map<CoverageReportAdapter, List<File>> detectReports(List<FilePath> dete
return results;
}

private CoverageResult aggregatedToReport(List<CoverageResult> results) {
/**
* Aggregate results to aggregated report
*
* @param results results will be aggregated
* @return aggregated report
*/
private CoverageResult aggregatedResults(Map<CoverageReportAdapter, List<CoverageResult>> results) {
CoverageResult report = new CoverageResult(CoverageElement.AGGREGATED_REPORT, null, "All reports");
for (CoverageResult result : results) {
result.addParent(report);
for (List<CoverageResult> resultList : results.values()) {
for (CoverageResult result : resultList) {
result.addParent(report);
}
}
return report;
}
Expand Down Expand Up @@ -241,6 +338,11 @@ public void enableAutoDetect(String autoDetectPath) {
this.autoDetectPath = autoDetectPath;
}

/**
* Getter for property 'enableAutoDetect'
*
* @return isEnableAutoDetect
*/
public boolean isEnableAutoDetect() {
return enableAutoDetect;
}
Expand Down Expand Up @@ -268,20 +370,27 @@ public FilePath[] invoke(File f, VirtualChannel channel) throws IOException, Int
}
}


public List<Threshold> getGlobalThresholds() {
return globalThresholds;
}

public static void saveReport(Run<?, ?> run, CoverageResult report) throws IOException {
/**
* Save {@link CoverageResult} in build directory.
*
* @param run build
* @param report report
*/
public static void saveCoverageResult(Run<?, ?> run, CoverageResult report) throws IOException {
File reportFile = new File(run.getRootDir(), DEFAULT_REPORT_SAVE_NAME);

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(reportFile))) {
oos.writeObject(report);
}
}

public static CoverageResult recoverReport(Run<?, ?> run) throws IOException, ClassNotFoundException {
/**
* Recover {@link CoverageResult} from build directory.
*
* @param run build
* @return Coverage result
*/
public static CoverageResult recoverCoverageResult(Run<?, ?> run) throws IOException, ClassNotFoundException {
File reportFile = new File(run.getRootDir(), DEFAULT_REPORT_SAVE_NAME);

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(reportFile))) {
Expand Down
22 changes: 13 additions & 9 deletions src/main/java/io/jenkins/plugins/coverage/CoveragePublisher.java
Expand Up @@ -14,8 +14,8 @@
import hudson.tasks.Recorder;
import io.jenkins.plugins.coverage.adapter.CoverageReportAdapter;
import io.jenkins.plugins.coverage.adapter.CoverageReportAdapterDescriptor;
import io.jenkins.plugins.coverage.exception.CoverageException;
import io.jenkins.plugins.coverage.targets.CoverageMetric;
import io.jenkins.plugins.coverage.targets.CoverageResult;
import io.jenkins.plugins.coverage.threshold.Threshold;
import jenkins.tasks.SimpleBuildStep;
import net.sf.json.JSONObject;
Expand All @@ -35,7 +35,11 @@ public class CoveragePublisher extends Recorder implements SimpleBuildStep {
private List<CoverageReportAdapter> adapters;
private List<Threshold> globalThresholds;

private String autoDetectPath = CoveragePublisherDescriptor.AUTO_DETACT_PATH;
private boolean failedUnhealthy;
private boolean failedUnstable;

@Nonnull
private String autoDetectPath;

@DataBoundConstructor
public CoveragePublisher(String autoDetectPath) {
Expand All @@ -46,16 +50,18 @@ public CoveragePublisher(String autoDetectPath) {
public void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException {
listener.getLogger().println("Publishing Coverage report....");

CoverageProcessor processor = new CoverageProcessor(run, workspace, listener, adapters, globalThresholds);
CoverageProcessor processor = new CoverageProcessor(run, workspace, listener);

if (!StringUtils.isEmpty(autoDetectPath)) {
processor.enableAutoDetect(autoDetectPath);
}

CoverageResult result = processor.processCoverageReport();

CoverageAction action = new CoverageAction(result);
run.addAction(action);
try {
processor.performCoverageReport(getAdapters(), globalThresholds);
} catch (CoverageException e) {
listener.getLogger().println(e.getMessage());
run.setResult(Result.FAILURE);
}
}

@Override
Expand Down Expand Up @@ -94,8 +100,6 @@ public void setAutoDetectPath(@Nonnull String autoDetectPath) {
@Extension
public static final class CoveragePublisherDescriptor extends BuildStepDescriptor<Publisher> {

public static final String AUTO_DETACT_PATH = "*.xml";

public CoveragePublisherDescriptor() {
super(CoveragePublisher.class);
load();
Expand Down

0 comments on commit bf05960

Please sign in to comment.