diff --git a/plugin/pom.xml b/plugin/pom.xml index 084e10c01..a10cb6c50 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -5,7 +5,7 @@ org.jvnet.hudson.plugins analysis-pom - 6.0.0 + 6.1.0 @@ -35,11 +35,17 @@ 1.81 2.9.0 - 0.13.0 + 0.15.0 5.4.0-2-rc759.8b_4e78286216 1.29.0-3-rc207.f000c20b_dea_5 - 3.0.0-rc679.e40704a_a_f29f 1.11.0 + + 3.6.3-1-rc365.70899fb_d9e1d + 3.0.0-rc693.c098b_871ea_49 + 5.2.2-1-rc442.6631330fec41 + 6.3.0-1-rc517.f6b_6e5a_dd4ef + + 2.0.0-rc1380.c93c627cd828 @@ -139,11 +145,12 @@ io.jenkins.plugins bootstrap5-api - 5.2.0-3 + ${bootstrap5-api.version} io.jenkins.plugins jquery3-api + ${jquery3-api.version} io.jenkins.plugins @@ -152,7 +159,7 @@ io.jenkins.plugins forensics-api - 2.0.0-rc1380.c93c627cd828 + ${forensics-api.version} io.jenkins.plugins @@ -162,6 +169,7 @@ io.jenkins.plugins font-awesome-api + ${font-awesome-api.version} io.jenkins.plugins diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java index 5b55fc18c..412afaf75 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java @@ -1,5 +1,7 @@ package io.jenkins.plugins.coverage.metrics.charts; +import java.util.Optional; + import edu.hm.hafner.echarts.ItemStyle; import edu.hm.hafner.echarts.Label; import edu.hm.hafner.echarts.LabeledTreeMapNode; @@ -38,7 +40,8 @@ public class TreeMapNodeConverter { */ public LabeledTreeMapNode toTreeChartModel(final Node node, final Metric metric, final ColorProvider colorProvider) { var tree = mergePackages(node); - LabeledTreeMapNode root = toTreeMapNode(tree, metric, colorProvider); + LabeledTreeMapNode root = toTreeMapNode(tree, metric, colorProvider).orElse( + new LabeledTreeMapNode(node.getPath(), node.getName())); for (LabeledTreeMapNode child : root.getChildren()) { child.collapseEmptyPackages(); } @@ -55,18 +58,18 @@ private Node mergePackages(final Node node) { return node; } - private LabeledTreeMapNode toTreeMapNode(final Node node, final Metric metric, + private Optional toTreeMapNode(final Node node, final Metric metric, final ColorProvider colorProvider) { var value = node.getValue(metric); if (value.isPresent()) { var rootValue = value.get(); if (rootValue instanceof Coverage) { - return createCoverageTree((Coverage) rootValue, colorProvider, node, metric); + return Optional.of(createCoverageTree((Coverage) rootValue, colorProvider, node, metric)); } // TODO: does it make sense to render the other metrics? } - return new LabeledTreeMapNode(node.getPath(), node.getName()); + return Optional.empty(); } private LabeledTreeMapNode createCoverageTree(final Coverage coverage, final ColorProvider colorProvider, final Node node, @@ -91,6 +94,7 @@ private LabeledTreeMapNode createCoverageTree(final Coverage coverage, final Col node.getChildren().stream() .map(n -> toTreeMapNode(n, metric, colorProvider)) + .flatMap(Optional::stream) .forEach(treeNode::insertNode); return treeNode; diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/Baseline.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/Baseline.java index d338f0eac..709440405 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/Baseline.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/Baseline.java @@ -27,23 +27,23 @@ public enum Baseline { * Coverage of the modified lines (e.g., within the modified lines of a pull or merge request) will focus on new or * modified code only. */ - MODIFIED_LINES(Messages._Baseline_MODIFIED_LINES(), "changeCoverage", CoverageLevel::getDisplayColorsOfCoverageLevel), + MODIFIED_LINES(Messages._Baseline_MODIFIED_LINES(), "modifiedLinesCoverage", CoverageLevel::getDisplayColorsOfCoverageLevel), /** * Difference between the project coverage and the modified lines coverage of the current build. Teams can use this delta * value to ensure that the coverage of pull requests is better than the whole project coverage. */ - MODIFIED_LINES_DELTA(Messages._Baseline_MODIFIED_LINES_DELTA(), "changeCoverage", + MODIFIED_LINES_DELTA(Messages._Baseline_MODIFIED_LINES_DELTA(), "modifiedLinesCoverage", CoverageChangeTendency::getDisplayColorsForTendency), /** * Coverage of the modified files (e.g., within the files that have been touched in a pull or merge request) will * focus on new or modified code only. */ - MODIFIED_FILES(Messages._Baseline_MODIFIED_FILES(), "fileCoverage", CoverageLevel::getDisplayColorsOfCoverageLevel), + MODIFIED_FILES(Messages._Baseline_MODIFIED_FILES(), "modifiedFilesCoverage", CoverageLevel::getDisplayColorsOfCoverageLevel), /** * Difference between the project coverage and the modified file coverage of the current build. Teams can use this delta * value to ensure that the coverage of pull requests is better than the whole project coverage. */ - MODIFIED_FILES_DELTA(Messages._Baseline_MODIFIED_FILES_DELTA(), "fileCoverage", CoverageChangeTendency::getDisplayColorsForTendency), + MODIFIED_FILES_DELTA(Messages._Baseline_MODIFIED_FILES_DELTA(), "modifiedFilesCoverage", CoverageChangeTendency::getDisplayColorsForTendency), /** * Indirect changes of the overall code coverage that are not part of the changed code. These changes might occur, * if new tests will be added without touching the underlying code under test. diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/ElementFormatter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/ElementFormatter.java index 2f595702a..79f77b030 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/ElementFormatter.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/ElementFormatter.java @@ -1,8 +1,12 @@ package io.jenkins.plugins.coverage.metrics.model; +import java.util.List; import java.util.Locale; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.Fraction; @@ -11,6 +15,7 @@ import edu.hm.hafner.metric.FractionValue; import edu.hm.hafner.metric.IntegerValue; import edu.hm.hafner.metric.Metric; +import edu.hm.hafner.metric.Node; import edu.hm.hafner.metric.Percentage; import edu.hm.hafner.metric.Value; @@ -134,6 +139,7 @@ public String formatDetails(final Value value, final Locale locale) { * * @param value * the value to format + * * @return the formatted value as plain text */ public String formatAdditionalInformation(final Value value) { @@ -184,10 +190,11 @@ public boolean showColors(final Value value) { public DisplayColors getDisplayColors(final Baseline baseline, final Value value) { var defaultColorProvider = ColorProviderFactory.createDefaultColorProvider(); if (value instanceof Coverage) { - return baseline.getDisplayColors(((Coverage)value).getCoveredPercentage().toDouble(), defaultColorProvider); + return baseline.getDisplayColors(((Coverage) value).getCoveredPercentage().toDouble(), + defaultColorProvider); } else if (value instanceof FractionValue) { - return baseline.getDisplayColors(((FractionValue)value).getFraction().doubleValue(), defaultColorProvider); + return baseline.getDisplayColors(((FractionValue) value).getFraction().doubleValue(), defaultColorProvider); } return ColorProvider.DEFAULT_COLOR; } @@ -370,6 +377,48 @@ public String getDisplayName(final Metric metric) { } } + /** + * Gets the display names of the existing {@link Metric coverage metrics}, sorted by the metrics ordinal. + * + * @return the sorted metric display names + */ + public List getSortedCoverageDisplayNames() { + return Metric.getCoverageMetrics().stream() + .map(this::getDisplayName) + .collect(Collectors.toList()); + } + + /** + * Formats a stream of values to their display representation by using the given locale. + * + * @param values + * The values to be formatted + * @param locale + * The locale to be used for formatting + * + * @return the formatted values in the origin order of the stream + */ + public List getFormattedValues(final Stream values, final Locale locale) { + return values.map(value -> formatDetails(value, locale)).collect(Collectors.toList()); + } + + /** + * Returns a stream of {@link Coverage} values for the given root node sorted by the metric ordinal. + * + * @param coverage + * The coverage root node + * + * @return a stream containing the existent coverage values + */ + public Stream getSortedCoverageValues(final Node coverage) { + return Metric.getCoverageMetrics() + .stream() + .map(m -> m.getValueFor(coverage)) + .flatMap(Optional::stream) + .filter(value -> value instanceof Coverage) + .map(Coverage.class::cast); + } + /** * Returns a localized human-readable label for the specified metric. * diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacade.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacade.java index 4afd6bb58..499fcd737 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacade.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacade.java @@ -163,7 +163,7 @@ File createFileInBuildFolder(final File buildResults, final String id, final Str } /** - * Filters the sourcecode coverage highlighting for analyzing the change coverage only. + * Filters the sourcecode coverage highlighting for analyzing the modified lines coverage only. * * @param content * The original HTML content @@ -172,9 +172,9 @@ File createFileInBuildFolder(final File buildResults, final String id, final Str * * @return the filtered HTML sourcecode view */ - public String calculateChangeCoverageSourceCode(final String content, final FileNode fileNode) { + public String calculateModifiedLinesCoverageSourceCode(final String content, final FileNode fileNode) { Set lines = fileNode.getLinesWithCoverage(); - lines.retainAll(fileNode.getChangedLines()); + lines.retainAll(fileNode.getModifiedLines()); Set linesAsText = lines.stream().map(String::valueOf).collect(Collectors.toSet()); Document doc = Jsoup.parse(content, Parser.xmlParser()); int maxLine = Integer.parseInt(Objects.requireNonNull( diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodePainter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodePainter.java index c468c57b4..1f45816ca 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodePainter.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodePainter.java @@ -18,7 +18,7 @@ import org.apache.commons.io.FileUtils; import edu.hm.hafner.metric.FileNode; -import edu.hm.hafner.metric.Metric; +import edu.hm.hafner.metric.Mutation; import edu.hm.hafner.util.FilteredLog; import edu.umd.cs.findbugs.annotations.NonNull; @@ -328,18 +328,26 @@ private enum Type { private final int[] linesToPaint; private final int[] coveredPerLine; private final int[] missedPerLine; - private final Type type; + private final int[] survivedPerLine; + private final int[] killedPerLine; PaintedNode(final FileNode file) { path = file.getPath(); + linesToPaint = file.getLinesWithCoverage().stream().mapToInt(i -> i).toArray(); coveredPerLine = file.getCoveredCounters(); missedPerLine = file.getMissedCounters(); - if (file.containsMetric(Metric.MUTATION)) { // FIXME: this needs to be generalized - type = Type.MUTATION; - } - else { - type = Type.COVERAGE; + + survivedPerLine = new int[linesToPaint.length]; + killedPerLine = new int[linesToPaint.length]; + + for (Mutation mutation : file.getMutations()) { // FIXME: this needs to be generalized + if (mutation.hasSurvived()) { + survivedPerLine[findLine(mutation.getLine())]++; + } + else if (mutation.isKilled()) { + killedPerLine[findLine(mutation.getLine())]++; + } } } @@ -347,10 +355,6 @@ public String getPath() { return path; } - public boolean isMutation() { - return type == Type.MUTATION; - } - public boolean isPainted(final int line) { return findLine(line) >= 0; } @@ -367,6 +371,14 @@ public int getMissed(final int line) { return getCounter(line, missedPerLine); } + public int getSurvived(final int line) { + return getCounter(line, survivedPerLine); + } + + public int getKilled(final int line) { + return getCounter(line, killedPerLine); + } + private int getCounter(final int line, final int[] counters) { var index = findLine(line); if (index >= 0) { diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java index 0e3314ba8..2e73f5ace 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java @@ -29,17 +29,25 @@ private void paintLine(final int line, final String content, final PaintedNode p int covered = paint.getCovered(line); int missed = paint.getMissed(line); - output.write("\n"); + int survived = paint.getSurvived(line); + int killed = paint.getKilled(line); + + output.write("\n"); output.write("" + line + "\n"); String display; - if (covered + missed > 1) { + + if (survived + killed > 0) { + display = String.format("%d/%d", killed, survived + killed); + } + else if (covered + missed > 1) { display = String.format("%d/%d", covered, covered + missed); } else { display = String.valueOf(covered); } + output.write("" + display + "\n"); } @@ -60,11 +68,12 @@ private void paintLine(final int line, final String content, final PaintedNode p output.write("\n"); } - private String selectColor(final int covered, final int missed) { + private String selectColor(final int covered, final int missed, final int survived) { + // TODO: what colors should be used for the different cases: Mutations or Branches? if (covered == 0) { return "coverNone"; } - else if (missed == 0) { + else if (missed == 0 && survived == 0) { return "coverFull"; } else { @@ -72,8 +81,9 @@ else if (missed == 0) { } } - private String getTooltip(final PaintedNode paint, final int missed, final int covered) { - var tooltip = getTooltipValue(paint, missed, covered); + private String getTooltip(final PaintedNode paint, + final int missed, final int covered, final int survived, final int killed) { + var tooltip = getTooltipValue(paint, missed, covered, survived, killed); if (StringUtils.isBlank(tooltip)) { return StringUtils.EMPTY; } @@ -81,27 +91,30 @@ private String getTooltip(final PaintedNode paint, final int missed, final int c } // TODO: Extract into classes so that we can paint the mutations as well - private String getTooltipValue(final PaintedNode paint, final int missed, final int covered) { - if (paint.isMutation()) { - if (missed + covered > 1) { - return String.format("Killed: %d, Survived: %d", covered, missed); - } - if (missed == 1) { - return "Survived: 1"; - } - return "Killed: 1"; + private String getTooltipValue(final PaintedNode paint, + final int missed, final int covered, final int survived, final int killed) { + + if (survived + killed > 1) { + return String.format("Mutations survived: %d, mutations killed: %d", survived, killed); + } + if (survived == 1) { + return "One survived mutation"; } + if (killed == 1) { + return "One killed mutation"; + } + if (covered + missed > 1) { if (missed == 0) { - return "Line covered with full branch coverage"; + return "All branches covered"; } - return String.format("Line covered, branch coverage: %d/%d", covered, covered + missed); + return String.format("Partially covered, branch coverage: %d/%d", covered, covered + missed); } else if (covered == 1) { - return "Line covered at least once"; + return "Covered at least once"; } else { - return "Line not covered"; // No tooltip required + return "Not covered"; } } } diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java index 0fcb874da..d12a7ee62 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java @@ -64,19 +64,19 @@ FileNode getOriginalFile() { @Override public DetailedCell getLineCoverageDelta() { - return createColoredChangeCoverageDeltaColumn(Metric.LINE); + return createColoredModifiedLinesCoverageDeltaColumn(Metric.LINE); } @Override public DetailedCell getBranchCoverageDelta() { - return createColoredChangeCoverageDeltaColumn(Metric.BRANCH); + return createColoredModifiedLinesCoverageDeltaColumn(Metric.BRANCH); } - DetailedCell createColoredChangeCoverageDeltaColumn(final Metric metric) { - Coverage changeCoverage = getFile().getTypedValue(metric, Coverage.nullObject(metric)); - if (changeCoverage.isSet()) { + DetailedCell createColoredModifiedLinesCoverageDeltaColumn(final Metric metric) { + Coverage modifiedLinesCoverage = getFile().getTypedValue(metric, Coverage.nullObject(metric)); + if (modifiedLinesCoverage.isSet()) { return createColoredCoverageDeltaColumn(metric, - changeCoverage.delta(originalFile.getTypedValue(metric, Coverage.nullObject(metric)))); + modifiedLinesCoverage.delta(originalFile.getTypedValue(metric, Coverage.nullObject(metric)))); } return NO_COVERAGE; } diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction.java index d703a9508..52ed3128c 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildAction.java @@ -75,10 +75,16 @@ public final class CoverageBuildAction extends BuildAction implements Stap private final NavigableMap difference; /** The coverages filtered by changed lines of the associated change request. */ - private final List changeCoverage; + private final List modifiedLinesCoverage; /** The delta of the coverages of the associated change request with respect to the reference build. */ - private final NavigableMap changeCoverageDifference; + private final NavigableMap modifiedLinesCoverageDifference; + + /** The coverage of the modified lines. */ + private final List modifiedFilesCoverage; + + /** The coverage delta of the modified lines. */ + private final NavigableMap modifiedFilesCoverageDifference; /** The indirect coverage changes of the associated change request with respect to the reference build. */ private final List indirectCoverageChanges; @@ -87,7 +93,7 @@ public final class CoverageBuildAction extends BuildAction implements Stap CoverageXmlStream.registerConverters(XSTREAM2); XSTREAM2.registerLocalConverter(CoverageBuildAction.class, "difference", new MetricFractionMapConverter()); - XSTREAM2.registerLocalConverter(CoverageBuildAction.class, "changeCoverageDifference", + XSTREAM2.registerLocalConverter(CoverageBuildAction.class, "modifiedLinesCoverageDifference", new MetricFractionMapConverter()); } @@ -112,7 +118,7 @@ public final class CoverageBuildAction extends BuildAction implements Stap public CoverageBuildAction(final Run owner, final String id, final String optionalName, final String icon, final Node result, final QualityGateResult qualityGateResult, final FilteredLog log) { this(owner, id, optionalName, icon, result, qualityGateResult, log, NO_REFERENCE_BUILD, - new TreeMap<>(), List.of(), new TreeMap<>(), List.of()); + new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), new TreeMap<>(), List.of()); } /** @@ -136,9 +142,9 @@ public CoverageBuildAction(final Run owner, final String id, final String * the ID of the reference build * @param delta * delta of this build's coverages with respect to the reference build - * @param changeCoverage + * @param modifiedLinesCoverage * the coverages filtered by changed lines of the associated change request - * @param changeCoverageDifference + * @param modifiedLinesCoverageDifference * the delta of the coverages of the associated change request with respect to the reference build * @param indirectCoverageChanges * the indirect coverage changes of the associated change request with respect to the reference build @@ -148,11 +154,16 @@ public CoverageBuildAction(final Run owner, final String id, final String final Node result, final QualityGateResult qualityGateResult, final FilteredLog log, final String referenceBuildId, final NavigableMap delta, - final List changeCoverage, - final NavigableMap changeCoverageDifference, + final List modifiedLinesCoverage, + final NavigableMap modifiedLinesCoverageDifference, + final List modifiedFilesCoverage, + final NavigableMap modifiedFilesCoverageDifference, final List indirectCoverageChanges) { - this(owner, id, optionalName, icon, result, qualityGateResult, log, referenceBuildId, delta, changeCoverage, - changeCoverageDifference, indirectCoverageChanges, true); + this(owner, id, optionalName, icon, result, qualityGateResult, log, referenceBuildId, delta, + modifiedLinesCoverage, + modifiedLinesCoverageDifference, modifiedFilesCoverage, modifiedFilesCoverageDifference, + indirectCoverageChanges, + true); } @VisibleForTesting @@ -161,8 +172,10 @@ public CoverageBuildAction(final Run owner, final String id, final String final Node result, final QualityGateResult qualityGateResult, final FilteredLog log, final String referenceBuildId, final NavigableMap delta, - final List changeCoverage, - final NavigableMap changeCoverageDifference, + final List modifiedLinesCoverage, + final NavigableMap modifiedLinesCoverageDifference, + final List modifiedFilesCoverage, + final NavigableMap modifiedFilesCoverageDifference, final List indirectCoverageChanges, final boolean canSerialize) { super(owner, result, false); @@ -175,8 +188,10 @@ public CoverageBuildAction(final Run owner, final String id, final String projectValues = result.aggregateValues(); this.qualityGateResult = qualityGateResult; difference = delta; - this.changeCoverage = new ArrayList<>(changeCoverage); - this.changeCoverageDifference = changeCoverageDifference; + this.modifiedLinesCoverage = new ArrayList<>(modifiedLinesCoverage); + this.modifiedLinesCoverageDifference = modifiedLinesCoverageDifference; + this.modifiedFilesCoverage = new ArrayList<>(modifiedFilesCoverage); + this.modifiedFilesCoverageDifference = modifiedFilesCoverageDifference; this.indirectCoverageChanges = new ArrayList<>(indirectCoverageChanges); this.referenceBuildId = referenceBuildId; @@ -207,8 +222,8 @@ public ElementFormatter getFormatter() { } public CoverageStatistics getStatistics() { - return new CoverageStatistics(projectValues, difference, changeCoverage, changeCoverageDifference, - List.of(), new TreeMap<>()); + return new CoverageStatistics(projectValues, difference, modifiedLinesCoverage, modifiedLinesCoverageDifference, + modifiedFilesCoverage, modifiedFilesCoverageDifference); } /** @@ -218,7 +233,7 @@ public CoverageStatistics getStatistics() { */ @SuppressWarnings("unused") // Called by jelly view public List getBaselines() { - return List.of(Baseline.PROJECT, Baseline.MODIFIED_LINES, Baseline.INDIRECT); + return List.of(Baseline.PROJECT, Baseline.MODIFIED_FILES, Baseline.MODIFIED_LINES, Baseline.INDIRECT); } /** @@ -273,6 +288,19 @@ public List getAllValues(final Baseline baseline) { return getValueStream(baseline).collect(Collectors.toList()); } + public NavigableMap getAllDeltas(final Baseline deltaBaseline) { + if (deltaBaseline == Baseline.PROJECT_DELTA) { + return difference; + } + else if (deltaBaseline == Baseline.MODIFIED_LINES_DELTA) { + return modifiedLinesCoverageDifference; + } + else if (deltaBaseline == Baseline.MODIFIED_FILES_DELTA) { + return modifiedFilesCoverageDifference; + } + throw new NoSuchElementException("No delta baseline: " + deltaBaseline); + } + /** * Returns all important values for the specified baseline. * @@ -288,6 +316,12 @@ public List getValues(final Baseline baseline) { return filterImportantMetrics(getValueStream(baseline)); } + public Optional getValueForMetric(final Baseline baseline, final Metric metric) { + return getAllValues(baseline).stream() + .filter(value -> value.getMetric() == metric) + .findFirst(); + } + private List filterImportantMetrics(final Stream values) { return values.filter(v -> getMetricsForSummary().contains(v.getMetric())) .collect(Collectors.toList()); @@ -298,7 +332,10 @@ private Stream getValueStream(final Baseline baseline) { return projectValues.stream(); } if (baseline == Baseline.MODIFIED_LINES) { - return changeCoverage.stream(); + return modifiedLinesCoverage.stream(); + } + if (baseline == Baseline.MODIFIED_FILES) { + return modifiedFilesCoverage.stream(); } if (baseline == Baseline.INDIRECT) { return indirectCoverageChanges.stream(); @@ -316,7 +353,8 @@ private Stream getValueStream(final Baseline baseline) { */ @SuppressWarnings("unused") // Called by jelly view public boolean hasDelta(final Baseline baseline) { - return baseline == Baseline.PROJECT || baseline == Baseline.MODIFIED_LINES; + return baseline == Baseline.PROJECT || baseline == Baseline.MODIFIED_LINES + || baseline == Baseline.MODIFIED_FILES; } /** @@ -334,7 +372,11 @@ public boolean hasDelta(final Baseline baseline, final Metric metric) { return difference.containsKey(metric); } if (baseline == Baseline.MODIFIED_LINES) { - return changeCoverageDifference.containsKey(metric) + return modifiedLinesCoverageDifference.containsKey(metric) + && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); + } + if (baseline == Baseline.MODIFIED_FILES) { + return modifiedFilesCoverageDifference.containsKey(metric) && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); } if (baseline == Baseline.INDIRECT) { @@ -343,6 +385,37 @@ public boolean hasDelta(final Baseline baseline, final Metric metric) { throw new NoSuchElementException("No such baseline: " + baseline); } + /** + * Returns whether a value for the specified metric exists. + * + * @param baseline + * the baseline to use + * @param metric + * the metric to check + * + * @return {@code true} if a value is available for the specified metric, {@code false} otherwise + */ + public boolean hasValue(final Baseline baseline, final Metric metric) { + return getAllValues(baseline).stream() + .anyMatch(v -> v.getMetric() == metric); + } + + /** + * Returns a formatted and localized String representation of the value for the specified metric (with respect to + * the given baseline). + * + * @param baseline + * the baseline to use + * @param metric + * the metric to get the delta for + * + * @return the formatted value + */ + public String formatValue(final Baseline baseline, final Metric metric) { + var value = getValueForMetric(baseline, metric); + return value.isPresent() ? FORMATTER.formatValue(value.get()) : Messages.Coverage_Not_Available(); + } + /** * Returns a formatted and localized String representation of the delta for the specified metric (with respect to * the given baseline). @@ -364,7 +437,13 @@ public String formatDelta(final Baseline baseline, final Metric metric) { } if (baseline == Baseline.MODIFIED_LINES) { if (hasDelta(baseline, metric)) { - return FORMATTER.formatDelta(changeCoverageDifference.get(metric), metric, + return FORMATTER.formatDelta(modifiedLinesCoverageDifference.get(metric), metric, + Functions.getCurrentLocale()); + } + } + if (baseline == Baseline.MODIFIED_FILES) { + if (hasDelta(baseline, metric)) { + return FORMATTER.formatDelta(modifiedFilesCoverageDifference.get(metric), metric, Functions.getCurrentLocale()); } } diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java new file mode 100644 index 000000000..5ab38620a --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java @@ -0,0 +1,426 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.math.Fraction; + +import edu.hm.hafner.metric.Coverage; +import edu.hm.hafner.metric.FileNode; +import edu.hm.hafner.metric.Metric; +import edu.hm.hafner.metric.Node; +import edu.hm.hafner.util.VisibleForTesting; + +import hudson.Functions; +import hudson.model.TaskListener; + +import io.jenkins.plugins.checks.api.ChecksAnnotation; +import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationBuilder; +import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationLevel; +import io.jenkins.plugins.checks.api.ChecksConclusion; +import io.jenkins.plugins.checks.api.ChecksDetails; +import io.jenkins.plugins.checks.api.ChecksDetails.ChecksDetailsBuilder; +import io.jenkins.plugins.checks.api.ChecksOutput.ChecksOutputBuilder; +import io.jenkins.plugins.checks.api.ChecksPublisherFactory; +import io.jenkins.plugins.checks.api.ChecksStatus; +import io.jenkins.plugins.coverage.metrics.model.Baseline; +import io.jenkins.plugins.coverage.metrics.model.ElementFormatter; +import io.jenkins.plugins.coverage.metrics.steps.CoverageRecorder.ChecksAnnotationScope; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.QualityGateStatus; + +/** + * Publishes coverage as Checks to SCM platforms. + * + * @author Florian Orendi + */ +class CoverageChecksPublisher { + private static final ElementFormatter FORMATTER = new ElementFormatter(); + + private final CoverageBuildAction action; + private final JenkinsFacade jenkinsFacade; + private final String checksName; + private final ChecksAnnotationScope annotationScope; + + CoverageChecksPublisher(final CoverageBuildAction action, final String checksName, + final ChecksAnnotationScope annotationScope) { + this(action, checksName, annotationScope, new JenkinsFacade()); + } + + @VisibleForTesting + CoverageChecksPublisher(final CoverageBuildAction action, + final String checksName, final ChecksAnnotationScope annotationScope, final JenkinsFacade jenkinsFacade) { + this.jenkinsFacade = jenkinsFacade; + this.action = action; + this.checksName = checksName; + this.annotationScope = annotationScope; + } + + /** + * Publishes the coverage report as Checks to SCM platforms. + * + * @param listener + * The task listener + */ + void publishCoverageReport(final TaskListener listener) { + var publisher = ChecksPublisherFactory.fromRun(action.getOwner(), listener); + publisher.publish(extractChecksDetails()); + } + + @VisibleForTesting + ChecksDetails extractChecksDetails() { + var output = new ChecksOutputBuilder() + .withTitle(getChecksTitle()) + .withSummary(getSummary()) + .withAnnotations(getAnnotations()) + .build(); + + return new ChecksDetailsBuilder() + .withName(checksName) + .withStatus(ChecksStatus.COMPLETED) + .withConclusion(getCheckConclusion(action.getQualityGateResult().getOverallStatus())) + .withDetailsURL(getCoverageReportBaseUrl()) + .withOutput(output) + .build(); + } + + private String getChecksTitle() { + return String.format("%s: %s", + FORMATTER.getDisplayName(Baseline.MODIFIED_LINES), + action.formatValue(Baseline.MODIFIED_LINES, Metric.LINE)); + } + + private String getSummary() { + var root = action.getResult(); + return getOverallCoverageSummary(root) + "\n\n" + + getQualityGatesSummary() + "\n\n" + + getProjectMetricsSummary(root); + } + + private List getAnnotations() { + if (annotationScope == ChecksAnnotationScope.SKIP) { + return List.of(); + } + + var tree = action.getResult(); + Node filtered; + if (annotationScope == ChecksAnnotationScope.ALL_LINES) { + filtered = tree; + } + else { + filtered = tree.filterByModifiedLines(); + } + + var annotations = new ArrayList(); + for (var fileNode : filtered.getAllFileNodes()) { + annotations.addAll(getMissingLines(fileNode)); + annotations.addAll(getPartiallyCoveredLines(fileNode)); + annotations.addAll(getSurvivedMutations(fileNode)); + } + return annotations; + } + + private Collection getMissingLines(final FileNode fileNode) { + var builder = createAnnotationBuilder(fileNode).withTitle("Not covered line"); + + return fileNode.getMissedLines().stream() + .map(line -> builder.withMessage("Line " + line + " is not covered by tests").withStartLine(line).build()) + .collect(Collectors.toList()); + } + + private Collection getSurvivedMutations(final FileNode fileNode) { + var builder = createAnnotationBuilder(fileNode).withTitle("Mutation survived"); + + return fileNode.getSurvivedMutations().entrySet().stream() + .map(entry -> builder.withMessage(createMutationMessage(entry.getKey(), entry.getValue())) + .withStartLine(entry.getKey()).build()) + .collect(Collectors.toList()); + } + + private String createMutationMessage(final int line, final int survived) { + if (survived == 1) { + return "One mutation survived in line " + line; + } + return String.format("%d mutations survived in line %d", survived, line); + } + + private Collection getPartiallyCoveredLines(final FileNode fileNode) { + var builder = createAnnotationBuilder(fileNode).withTitle("Partially covered line"); + + return fileNode.getPartiallyCoveredLines().entrySet().stream() + .map(entry -> builder.withMessage(createBranchMessage(entry.getKey(), entry.getValue())) + .withStartLine(entry.getKey()).build()) + .collect(Collectors.toList()); + } + + private String createBranchMessage(final int line, final int missed) { + if (missed == 1) { + return "Line " + line + " is only partially covered, one branch is missing"; + + } + return "Line " + line + " is only partially covered, %d branches are missing."; + } + + private ChecksAnnotationBuilder createAnnotationBuilder(final FileNode fileNode) { + return new ChecksAnnotationBuilder() + .withPath(fileNode.getPath()) + .withAnnotationLevel(ChecksAnnotationLevel.WARNING); + } + + private String getCoverageReportBaseUrl() { + return jenkinsFacade.getAbsoluteUrl(action.getOwner().getUrl(), action.getUrlName()); + } + + private String getOverallCoverageSummary(final Node root) { + String sectionHeader = getSectionHeader(2, Messages.Checks_Summary()); + + var modifiedFilesCoverageRoot = root.filterByModifiedFiles(); + var modifiedLinesCoverageRoot = root.filterByModifiedLines(); + var indirectlyChangedCoverage = root.filterByIndirectChanges(); + + var projectCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.PROJECT_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.PROJECT_DELTA.getUrl()))); + var modifiedFilesCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.MODIFIED_FILES_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.MODIFIED_FILES_DELTA.getUrl()))); + var modifiedLinesCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.MODIFIED_LINES_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.MODIFIED_LINES_DELTA.getUrl()))); + var indirectCoverageChangesHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.INDIRECT.getTitle(), + getCoverageReportBaseUrl() + Baseline.INDIRECT.getUrl()))); + + var projectCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, Baseline.PROJECT)); + var projectCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, Baseline.PROJECT)); + var projectCoverageComplexity = getBulletListItem(2, formatRootValueOfMetric(root, Metric.COMPLEXITY_DENSITY)); + var projectCoverageLoc = getBulletListItem(2, formatRootValueOfMetric(root, Metric.LOC)); + + var modifiedFilesCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, Baseline.MODIFIED_FILES)); + var modifiedFilesCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, Baseline.MODIFIED_FILES)); + var modifiedFilesCoverageComplexity = getBulletListItem(2, + formatRootValueOfMetric(modifiedFilesCoverageRoot, Metric.COMPLEXITY_DENSITY)); + var modifiedFilesCoverageLoc = getBulletListItem(2, + formatRootValueOfMetric(modifiedFilesCoverageRoot, Metric.LOC)); + + var modifiedLinesCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, Baseline.MODIFIED_LINES)); + var modifiedLinesCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, Baseline.MODIFIED_LINES)); + var modifiedLinesCoverageLoc = getBulletListItem(2, + formatRootValueOfMetric(modifiedLinesCoverageRoot, Metric.LOC)); + + var indirectCoverageChangesLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, Baseline.INDIRECT)); + var indirectCoverageChangesBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, Baseline.INDIRECT)); + var indirectCoverageChangesLoc = getBulletListItem(2, + formatRootValueOfMetric(indirectlyChangedCoverage, Metric.LOC)); + + return sectionHeader + + projectCoverageHeader + + projectCoverageLine + + projectCoverageBranch + + projectCoverageComplexity + + projectCoverageLoc + + modifiedFilesCoverageHeader + + modifiedFilesCoverageLine + + modifiedFilesCoverageBranch + + modifiedFilesCoverageComplexity + + modifiedFilesCoverageLoc + + modifiedLinesCoverageHeader + + modifiedLinesCoverageLine + + modifiedLinesCoverageBranch + + modifiedLinesCoverageLoc + + indirectCoverageChangesHeader + + indirectCoverageChangesLine + + indirectCoverageChangesBranch + + indirectCoverageChangesLoc; + } + + /** + * Checks overview regarding the quality gate status. + * + * @return the markdown string representing the status summary + */ + // TODO: expand with summary of status of each defined quality gate + private String getQualityGatesSummary() { + return getSectionHeader(2, + Messages.Checks_QualityGates(action.getQualityGateResult().getOverallStatus().name())); + } + + private String getProjectMetricsSummary(final Node result) { + String sectionHeader = getSectionHeader(2, Messages.Checks_ProjectOverview()); + + List coverageDisplayNames = FORMATTER.getSortedCoverageDisplayNames(); + String header = formatRow(coverageDisplayNames); + String headerSeparator = formatRow( + getTableSeparators(ColumnAlignment.CENTER, coverageDisplayNames.size())); + + String projectCoverageName = String.format("|%s **%s**", Icon.WHITE_CHECK_MARK.markdown, + FORMATTER.getDisplayName(Baseline.PROJECT)); + List projectCoverage = FORMATTER.getFormattedValues(FORMATTER.getSortedCoverageValues(result), + Functions.getCurrentLocale()); + String projectCoverageRow = projectCoverageName + formatRow(projectCoverage); + + String projectCoverageDeltaName = String.format("|%s **%s**", Icon.CHART_UPWARDS_TREND.markdown, + FORMATTER.getDisplayName(Baseline.PROJECT_DELTA)); + Collection projectCoverageDelta = formatCoverageDelta(Metric.getCoverageMetrics(), + action.getAllDeltas(Baseline.PROJECT_DELTA)); + String projectCoverageDeltaRow = + projectCoverageDeltaName + formatRow(projectCoverageDelta); + + return sectionHeader + + header + + headerSeparator + + projectCoverageRow + + projectCoverageDeltaRow; + } + + private String formatCoverageForMetric(final Metric metric, final Baseline baseline) { + return String.format("%s: %s / %s", FORMATTER.getDisplayName(metric), + action.formatValue(baseline, metric), action.formatDelta(baseline, metric)); + } + + private String formatRootValueOfMetric(final Node root, final Metric metric) { + var value = root.getValue(metric); + return value.map(FORMATTER::formatValueWithMetric) + .orElseGet(() -> FORMATTER.getDisplayName(metric) + ": " + Messages.Coverage_Not_Available()); + } + + private String formatText(final TextFormat format, final String text) { + switch (format) { + case BOLD: + return "**" + text + "**"; + case CURSIVE: + return "_" + text + "_"; + default: + return text; + } + } + + /** + * Formats the passed delta computation to a collection of its display representations, which is sorted by the + * metric ordinal. Also, a collection of required metrics is passed. This is used to fill not existent metrics which + * are required for the representation. Coverage deltas might not be existent if the reference does not contain a + * reference value of the metric. + * + * @param requiredMetrics + * The metrics which should be displayed + * @param deltas + * The delta calculation mapped by their metric + */ + private Collection formatCoverageDelta(final Collection requiredMetrics, + final NavigableMap deltas) { + var coverageDelta = new TreeMap(); + for (Metric metric : requiredMetrics) { + if (deltas.containsKey(metric)) { + var coverage = deltas.get(metric); + coverageDelta.putIfAbsent(metric, + FORMATTER.formatDelta(coverage, metric, Functions.getCurrentLocale()) + + getTrendIcon(coverage.doubleValue())); + } + else { + coverageDelta.putIfAbsent(metric, + FORMATTER.formatPercentage(Coverage.nullObject(metric), Functions.getCurrentLocale())); + } + } + return coverageDelta.values(); + } + + private String getTrendIcon(final double trend) { + if (trend > 0) { + return " " + Icon.ARROW_UP.markdown; + } + else if (trend < 0) { + return " " + Icon.ARROW_DOWN.markdown; + } + else { + return " " + Icon.ARROW_RIGHT.markdown; + } + } + + private List getTableSeparators(final ColumnAlignment alignment, final int count) { + switch (alignment) { + case LEFT: + return Collections.nCopies(count, ":---"); + case RIGHT: + return Collections.nCopies(count, "---:"); + case CENTER: + default: + return Collections.nCopies(count, ":---:"); + } + } + + private String getBulletListItem(final int level, final String text) { + int whitespaces = (level - 1) * 2; + return String.join("", Collections.nCopies(whitespaces, " ")) + "* " + text + "\n"; + } + + private String getUrlText(final String text, final String url) { + return String.format("[%s](%s)", text, url); + } + + private String formatRow(final Collection columns) { + StringBuilder row = new StringBuilder(); + for (Object column : columns) { + row.append(String.format("|%s", column)); + } + if (columns.size() > 0) { + row.append('|'); + } + row.append('\n'); + return row.toString(); + } + + private String getSectionHeader(final int level, final String text) { + return String.join("", Collections.nCopies(level, "#")) + " " + text + "\n\n"; + } + + private ChecksConclusion getCheckConclusion(final QualityGateStatus status) { + switch (status) { + case INACTIVE: + case PASSED: + return ChecksConclusion.SUCCESS; + case FAILED: + case WARNING: + return ChecksConclusion.FAILURE; + default: + throw new IllegalArgumentException("Unsupported quality gate status: " + status); + } + } + + private enum ColumnAlignment { + CENTER, + LEFT, + RIGHT + } + + private enum Icon { + WHITE_CHECK_MARK(":white_check_mark:"), + CHART_UPWARDS_TREND(":chart_with_upwards_trend:"), + ARROW_UP(":arrow_up:"), + ARROW_RIGHT(":arrow_right:"), + ARROW_DOWN(":arrow_down:"); + + private final String markdown; + + Icon(final String markdown) { + this.markdown = markdown; + } + } + + private enum TextFormat { + BOLD, + CURSIVE + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder.java index f5dd7d441..dd549ff7c 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder.java @@ -63,6 +63,8 @@ */ @SuppressWarnings("checkstyle:ClassFanOutComplexity") public class CoverageRecorder extends Recorder { + static final String CHECKS_DEFAULT_NAME = "Code Coverage"; + static final String DEFAULT_ID = "coverage"; private static final ValidationUtilities VALIDATION_UTILITIES = new ValidationUtilities(); /** The coverage report symbol from the Ionicons plugin. */ @@ -73,6 +75,8 @@ public class CoverageRecorder extends Recorder { private String id = StringUtils.EMPTY; private String name = StringUtils.EMPTY; private boolean skipPublishingChecks = false; + private String checksName = StringUtils.EMPTY; + private ChecksAnnotationScope checksAnnotationScope = ChecksAnnotationScope.MODIFIED_LINES; private boolean failOnError = false; private boolean enabledForFailure = false; private boolean skipSymbolicLinks = false; @@ -187,6 +191,36 @@ public boolean isSkipPublishingChecks() { return skipPublishingChecks; } + /** + * Changes the default name for the SCM checks report. + * + * @param checksName + * the name that should be used for the SCM checks report + */ + @DataBoundSetter + public void setChecksName(final String checksName) { + this.checksName = checksName; + } + + public String getChecksName() { + return StringUtils.defaultIfBlank(checksName, CHECKS_DEFAULT_NAME); + } + + /** + * Sets the scope of the annotations that should be published to SCM checks. + * + * @param checksAnnotationScope + * the scope to use + */ + @DataBoundSetter + public void setChecksAnnotationScope(final ChecksAnnotationScope checksAnnotationScope) { + this.checksAnnotationScope = checksAnnotationScope; + } + + public ChecksAnnotationScope getChecksAnnotationScope() { + return checksAnnotationScope; + } + /** * Specify if traversal of symbolic links will be skipped during directory scanning for coverage reports. * @@ -342,23 +376,33 @@ void perform(final Run run, final FilePath workspace, final TaskListener t "No tools defined that will record the coverage files"); } else { - List results = recordCoverageResults(run, workspace, taskListener, resultHandler, log); - - if (!results.isEmpty()) { - CoverageReporter reporter = new CoverageReporter(); - reporter.publishAction(getActualId(), getName(), getIcon(), - Node.merge(results), run, workspace, taskListener, - getQualityGates(), - getScm(), getSourceDirectoriesPaths(), - getSourceCodeEncoding(), getSourceCodeRetention(), resultHandler); - } + perform(run, workspace, taskListener, resultHandler, log); } + } else { logHandler.log("Skipping execution of coverage recorder since overall result is '%s'", overallResult); } } + private void perform(final Run run, final FilePath workspace, final TaskListener taskListener, + final StageResultHandler resultHandler, final FilteredLog log) throws InterruptedException { + List results = recordCoverageResults(run, workspace, taskListener, resultHandler, log); + + if (!results.isEmpty()) { + CoverageReporter reporter = new CoverageReporter(); + var action = reporter.publishAction(getActualId(), getName(), getIcon(), + Node.merge(results), run, workspace, taskListener, + getQualityGates(), + getScm(), getSourceDirectoriesPaths(), + getSourceCodeEncoding(), getSourceCodeRetention(), resultHandler); + if (!skipPublishingChecks) { + var checksPublisher = new CoverageChecksPublisher(action, getChecksName(), getChecksAnnotationScope()); + checksPublisher.publishCoverageReport(taskListener); + } + } + } + private String getIcon() { var icons = tools.stream().map(CoverageTool::getParser).map(Parser::getIcon).collect(Collectors.toSet()); if (icons.size() == 1) { @@ -470,6 +514,23 @@ public ListBoxModel doFillSourceCodeRetentionItems(@AncestorInPath final Abstrac return new ListBoxModel(); } + /** + * Returns a model with all {@link ChecksAnnotationScope} scopes. + * + * @param project + * the project that is configured + * + * @return a model with all {@link ChecksAnnotationScope} scopes. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillChecksAnnotationScopeItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + return ChecksAnnotationScope.fillItems(); + } + return new ListBoxModel(); + } + /** * Returns a model with all available charsets. * @@ -528,4 +589,24 @@ public FormValidation doCheckId(@AncestorInPath final AbstractProject proj return VALIDATION_UTILITIES.validateId(id); } } + + /** + * Defines the scope of SCM checks annotations. + */ + enum ChecksAnnotationScope { + /** No annotations are created. */ + SKIP, + /** Only changed lines are annotated. */ + MODIFIED_LINES, + /** All lines are annotated. */ + ALL_LINES; + + static ListBoxModel fillItems() { + ListBoxModel items = new ListBoxModel(); + items.add(Messages.ChecksAnnotationScope_Skip(), ChecksAnnotationScope.SKIP.name()); + items.add(Messages.ChecksAnnotationScope_ModifiedLines(), ChecksAnnotationScope.MODIFIED_LINES.name()); + items.add(Messages.ChecksAnnotationScope_AllLines(), ChecksAnnotationScope.ALL_LINES.name()); + return items; + } + } } diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReporter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReporter.java index e21fdcce6..210eed362 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReporter.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageReporter.java @@ -1,5 +1,6 @@ package io.jenkins.plugins.coverage.metrics.steps; +import java.util.ArrayList; import java.util.List; import java.util.NavigableMap; import java.util.Optional; @@ -37,7 +38,8 @@ */ public class CoverageReporter { @SuppressWarnings("checkstyle:ParameterNumber") - void publishAction(final String id, final String optionalName, final String icon, final Node rootNode, final Run build, + CoverageBuildAction publishAction(final String id, final String optionalName, final String icon, final Node rootNode, + final Run build, final FilePath workspace, final TaskListener listener, final List qualityGates, final String scm, final Set sourceDirectories, final String sourceCodeEncoding, final SourceCodeRetention sourceCodeRetention, final StageResultHandler resultHandler) @@ -59,35 +61,44 @@ void publishAction(final String id, final String optionalName, final String icon log.logInfo("Calculating coverage deltas..."); - Node changeCoverageRoot = rootNode.filterChanges(); + Node modifiedLinesCoverageRoot = rootNode.filterByModifiedLines(); - NavigableMap changeCoverageDelta; - if (hasChangeCoverage(changeCoverageRoot)) { - changeCoverageDelta = changeCoverageRoot.computeDelta(rootNode); + NavigableMap modifiedLinesCoverageDelta; + List aggregatedModifiedFilesCoverage; + NavigableMap modifiedFilesCoverageDelta; + if (hasModifiedLinesCoverage(modifiedLinesCoverageRoot)) { + modifiedLinesCoverageDelta = modifiedLinesCoverageRoot.computeDelta(rootNode); + Node modifiedFilesCoverageRoot = rootNode.filterByModifiedFiles(); + aggregatedModifiedFilesCoverage = modifiedFilesCoverageRoot.aggregateValues(); + modifiedFilesCoverageDelta = modifiedFilesCoverageRoot.computeDelta(rootNode); } else { - changeCoverageDelta = new TreeMap<>(); - if (rootNode.hasChangedLines()) { + modifiedLinesCoverageDelta = new TreeMap<>(); + aggregatedModifiedFilesCoverage = new ArrayList<>(); + modifiedFilesCoverageDelta = new TreeMap<>(); + if (rootNode.hasModifiedLines()) { log.logInfo("No detected code changes affect the code coverage"); } } NavigableMap coverageDelta = rootNode.computeDelta(referenceRoot); - Node indirectCoverageChangesTree = rootNode.filterByIndirectlyChangedCoverage(); + Node indirectCoverageChangesTree = rootNode.filterByIndirectChanges(); QualityGateResult qualityGateResult; qualityGateResult = evaluateQualityGates(rootNode, log, - changeCoverageRoot.aggregateValues(), changeCoverageDelta, coverageDelta, + modifiedLinesCoverageRoot.aggregateValues(), modifiedLinesCoverageDelta, coverageDelta, resultHandler, qualityGates); action = new CoverageBuildAction(build, id, optionalName, icon, rootNode, qualityGateResult, log, referenceAction.getOwner().getExternalizableId(), coverageDelta, - changeCoverageRoot.aggregateValues(), - changeCoverageDelta, + modifiedLinesCoverageRoot.aggregateValues(), + modifiedLinesCoverageDelta, + aggregatedModifiedFilesCoverage, + modifiedFilesCoverageDelta, indirectCoverageChangesTree.aggregateValues()); if (sourceCodeRetention == SourceCodeRetention.MODIFIED) { - filesToStore = changeCoverageRoot.getAllFileNodes(); + filesToStore = modifiedLinesCoverageRoot.getAllFileNodes(); log.logInfo("-> Selecting %d modified files for source code painting", filesToStore.size()); } else { @@ -113,6 +124,7 @@ void publishAction(final String id, final String optionalName, final String icon logHandler.log(log); build.addAction(action); + return action; } private void createDeltaReports(final Node rootNode, final FilteredLog log, final Node referenceRoot, @@ -138,16 +150,18 @@ private void createDeltaReports(final Node rootNode, final FilteredLog log, fina catch (IllegalStateException exception) { log.logError("An error occurred while processing code and coverage changes:"); log.logError("-> Message: " + exception.getMessage()); - log.logError("-> Skipping calculating change coverage and indirect coverage changes"); + log.logError("-> Skipping calculating modified lines coverage, modified files coverage" + + " and indirect coverage changes"); } } private QualityGateResult evaluateQualityGates(final Node rootNode, final FilteredLog log, - final List changeCoverageDistribution, final NavigableMap changeCoverageDelta, + final List modifiedLinesCoverageDistribution, + final NavigableMap modifiedLinesCoverageDelta, final NavigableMap coverageDelta, final StageResultHandler resultHandler, final List qualityGates) { var statistics = new CoverageStatistics(rootNode.aggregateValues(), coverageDelta, - changeCoverageDistribution, changeCoverageDelta, List.of(), new TreeMap<>()); + modifiedLinesCoverageDistribution, modifiedLinesCoverageDelta, List.of(), new TreeMap<>()); CoverageQualityGateEvaluator evaluator = new CoverageQualityGateEvaluator(qualityGates, statistics); var qualityGateStatus = evaluator.evaluate(); if (qualityGateStatus.isInactive()) { @@ -159,7 +173,8 @@ private QualityGateResult evaluateQualityGates(final Node rootNode, final Filter log.logInfo("-> All quality gates have been passed"); } else { - var message = String.format("-> Some quality gates have been missed: overall result is %s", qualityGateStatus.getOverallStatus().getResult()); + var message = String.format("-> Some quality gates have been missed: overall result is %s", + qualityGateStatus.getOverallStatus().getResult()); log.logInfo(message); resultHandler.setResult(qualityGateStatus.getOverallStatus().getResult(), message); } @@ -169,14 +184,14 @@ private QualityGateResult evaluateQualityGates(final Node rootNode, final Filter return qualityGateStatus; } - private boolean hasChangeCoverage(final Node changeCoverageRoot) { - Optional lineCoverage = changeCoverageRoot.getValue(Metric.LINE); + private boolean hasModifiedLinesCoverage(final Node modifiedLinesCoverageRoot) { + Optional lineCoverage = modifiedLinesCoverageRoot.getValue(Metric.LINE); if (lineCoverage.isPresent()) { if (((edu.hm.hafner.metric.Coverage) lineCoverage.get()).isSet()) { return true; } } - Optional branchCoverage = changeCoverageRoot.getValue(Metric.BRANCH); + Optional branchCoverage = modifiedLinesCoverageRoot.getValue(Metric.BRANCH); return branchCoverage.filter(value -> ((edu.hm.hafner.metric.Coverage) value).isSet()).isPresent(); } diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageStep.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageStep.java index b795f499b..a215ebf57 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageStep.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageStep.java @@ -41,6 +41,8 @@ import io.jenkins.plugins.util.JenkinsFacade; import io.jenkins.plugins.util.ValidationUtilities; +import static io.jenkins.plugins.coverage.metrics.steps.CoverageRecorder.*; + /** * A pipeline {@code Step} that reads and parses coverage results in a build and adds the results to the persisted build * results. This step only provides the entry point for pipelines, the actual computation is delegated to an associated @@ -57,6 +59,8 @@ public class CoverageStep extends Step implements Serializable { private String id = StringUtils.EMPTY; private String name = StringUtils.EMPTY; private boolean skipPublishingChecks = false; + private String checksName = StringUtils.EMPTY; + private ChecksAnnotationScope checksAnnotationScope = ChecksAnnotationScope.MODIFIED_LINES; private boolean failOnError = false; private boolean enabledForFailure = false; private boolean skipSymbolicLinks = false; @@ -164,6 +168,36 @@ public boolean isSkipPublishingChecks() { return skipPublishingChecks; } + /** + * Changes the default name for the SCM checks report. + * + * @param checksName + * the name that should be used for the SCM checks report + */ + @DataBoundSetter + public void setChecksName(final String checksName) { + this.checksName = checksName; + } + + public String getChecksName() { + return checksName; + } + + /** + * Sets the scope of the annotations that should be published to SCM checks. + * + * @param checksAnnotationScope + * the scope to use + */ + @DataBoundSetter + public void setChecksAnnotationScope(final ChecksAnnotationScope checksAnnotationScope) { + this.checksAnnotationScope = checksAnnotationScope; + } + + public ChecksAnnotationScope getChecksAnnotationScope() { + return checksAnnotationScope; + } + /** * Specify if traversal of symbolic links will be skipped during directory scanning for coverage reports. * @@ -300,6 +334,8 @@ protected Void run() throws IOException, InterruptedException { recorder.setId(step.getId()); recorder.setName(step.getName()); recorder.setSkipPublishingChecks(step.isSkipPublishingChecks()); + recorder.setChecksName(step.getChecksName()); + recorder.setChecksAnnotationScope(step.getChecksAnnotationScope()); recorder.setFailOnError(step.isFailOnError()); recorder.setEnabledForFailure(step.isEnabledForFailure()); recorder.setScm(step.getScm()); @@ -363,6 +399,24 @@ public ListBoxModel doFillSourceCodeRetentionItems(@AncestorInPath final Abstrac return new ListBoxModel(); } + /** + * Returns a model with all {@link ChecksAnnotationScope} scopes. + * + * @param project + * the project that is configured + * + * @return a model with all {@link ChecksAnnotationScope} scopes. + */ + @POST + @SuppressWarnings("unused") // used by Stapler view data binding + public ListBoxModel doFillChecksAnnotationScopeItems(@AncestorInPath final AbstractProject project) { + if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + return ChecksAnnotationScope.fillItems(); + } + return new ListBoxModel(); + } + + /** * Returns a model with all available charsets. * diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTableModel.java index 7c1fc33d6..cf87b3373 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTableModel.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageTableModel.java @@ -91,6 +91,11 @@ public List getColumns() { .withHeaderClass(ColumnCss.HIDDEN) .build(); columns.add(fileHash); + TableColumn modified = new ColumnBuilder().withHeaderLabel("Modified") + .withDataPropertyKey("modified") + .withHeaderClass(ColumnCss.HIDDEN) + .build(); + columns.add(modified); TableColumn fileName = new ColumnBuilder().withHeaderLabel(Messages.Column_File()) .withDataPropertyKey("fileName") .withDetailedCell() @@ -199,6 +204,10 @@ public String getFileHash() { return String.valueOf(file.getPath().hashCode()); } + public boolean getModified() { + return file.hasModifiedLines(); + } + public DetailedCell getFileName() { return new DetailedCell<>(renderer.renderFileName(file.getName(), file.getPath()), file.getName()); } diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel.java index ec5aca317..ce1352a7f 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel.java @@ -2,7 +2,6 @@ import java.io.File; import java.io.IOException; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -68,7 +67,8 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen private static final SourceCodeFacade SOURCE_CODE_FACADE = new SourceCodeFacade(); static final String ABSOLUTE_COVERAGE_TABLE_ID = "absolute-coverage-table"; - static final String CHANGE_COVERAGE_TABLE_ID = "change-coverage-table"; + static final String MODIFIED_LINES_COVERAGE_TABLE_ID = "modified-lines-coverage-table"; + static final String MODIFIED_FILES_COVERAGE_TABLE_ID = "modified-files-coverage-table"; static final String INDIRECT_COVERAGE_TABLE_ID = "indirect-coverage-table"; private static final String INLINE_SUFFIX = "-inline"; private static final String INFO_MESSAGES_VIEW_URL = "info"; @@ -84,7 +84,8 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen private final Node node; private final String id; - private final Node changeCoverageTreeRoot; + private final Node modifiedLinesCoverageTreeRoot; + private final Node modifiedFilesCoverageTreeRoot; private final Node indirectCoverageChangesTreeRoot; private final Function trendChartFunction; @@ -109,8 +110,9 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen this.log = log; // initialize filtered coverage trees so that they will not be calculated multiple times - changeCoverageTreeRoot = node.filterChanges(); - indirectCoverageChangesTreeRoot = node.filterByIndirectlyChangedCoverage(); + modifiedLinesCoverageTreeRoot = node.filterByModifiedLines(); + modifiedFilesCoverageTreeRoot = node.filterByModifiedFiles(); + indirectCoverageChangesTreeRoot = node.filterByIndirectChanges(); this.trendChartFunction = trendChartFunction; } @@ -291,8 +293,8 @@ public TableModel getTableModel(final String tableId) { switch (actualId) { case ABSOLUTE_COVERAGE_TABLE_ID: return new CoverageTableModel(tableId, getNode(), renderer, colorProvider); - case CHANGE_COVERAGE_TABLE_ID: - return new ChangeCoverageTableModel(tableId, getNode(), changeCoverageTreeRoot, renderer, + case MODIFIED_LINES_COVERAGE_TABLE_ID: + return new ModifiedLinesCoverageTableModel(tableId, getNode(), modifiedLinesCoverageTreeRoot, renderer, colorProvider); case INDIRECT_COVERAGE_TABLE_ID: return new IndirectCoverageChangesTable(tableId, getNode(), indirectCoverageChangesTreeRoot, renderer, @@ -383,8 +385,8 @@ private String readSourceCode(final Node sourceNode, final String tableId) if (!content.isEmpty() && sourceNode instanceof FileNode) { FileNode fileNode = (FileNode) sourceNode; String cleanTableId = StringUtils.removeEnd(tableId, INLINE_SUFFIX); - if (CHANGE_COVERAGE_TABLE_ID.equals(cleanTableId)) { - return SOURCE_CODE_FACADE.calculateChangeCoverageSourceCode(content, fileNode); + if (MODIFIED_LINES_COVERAGE_TABLE_ID.equals(cleanTableId)) { + return SOURCE_CODE_FACADE.calculateModifiedLinesCoverageSourceCode(content, fileNode); } else if (INDIRECT_COVERAGE_TABLE_ID.equals(cleanTableId)) { return SOURCE_CODE_FACADE.calculateIndirectCoverageChangesSourceCode(content, fileNode); @@ -407,12 +409,12 @@ public boolean hasSourceCode() { } /** - * Checks whether change coverage exists. + * Checks whether modified lines coverage exists. * - * @return {@code true} whether change coverage exists, else {@code false} + * @return {@code true} whether modified lines coverage exists, else {@code false} */ - public boolean hasChangeCoverage() { - return getNode().getAllFileNodes().stream().anyMatch(FileNode::hasChangedLines); + public boolean hasModifiedLinesCoverage() { + return getNode().getAllFileNodes().stream().anyMatch(FileNode::hasModifiedLines); } /** @@ -489,14 +491,8 @@ public List getMetrics() { } private Stream sortCoverages() { - return coverage.getMetrics() - .stream() - .map(m -> m.getValueFor(coverage)) - .flatMap(Optional::stream) - .filter(value -> value instanceof Coverage) - .map(Coverage.class::cast) - .filter(c -> c.getTotal() > 1) // ignore elements that have a total of 1 - .sorted(Comparator.comparing(Coverage::getMetric)); + return ELEMENT_FORMATTER.getSortedCoverageValues(coverage) + .filter(c -> c.getTotal() > 1); // ignore elements that have a total of 1 } public List getCovered() { diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessor.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessor.java index 8f4ce0a75..d5f39c518 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessor.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessor.java @@ -61,7 +61,7 @@ public void attachChangedCodeLines(final Node coverageNode, final Map relevantChanges) { for (Change change : relevantChanges) { for (int i = change.getFromLine(); i <= change.getToLine(); i++) { - changedNode.addModifiedLine(i); + changedNode.addModifiedLines(i); } } } @@ -142,7 +142,7 @@ public void attachIndirectCoveragesChanges(final Node root, final Node reference private void attachIndirectCoverageChangeForFile(final FileNode fileNode, final SortedMap referenceCoverageMapping) { fileNode.getLinesWithCoverage().forEach(line -> { - if (!fileNode.hasChangedLine(line) && referenceCoverageMapping.containsKey(line)) { + if (!fileNode.hasModifiedLine(line) && referenceCoverageMapping.containsKey(line)) { int referenceCovered = referenceCoverageMapping.get(line); int covered = fileNode.getCoveredOfLine(line); if (covered != referenceCovered) { diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangeCoverageTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedLinesCoverageTableModel.java similarity index 67% rename from plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangeCoverageTableModel.java rename to plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedLinesCoverageTableModel.java index c6fbcd4bb..383b87c8b 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangeCoverageTableModel.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedLinesCoverageTableModel.java @@ -10,23 +10,23 @@ /** * A coverage table model that handles the modified lines of a change with respect to a result of a reference build. */ -class ChangeCoverageTableModel extends ChangesTableModel { - ChangeCoverageTableModel(final String id, final Node root, final Node changeRoot, +class ModifiedLinesCoverageTableModel extends ChangesTableModel { + ModifiedLinesCoverageTableModel(final String id, final Node root, final Node changeRoot, final RowRenderer renderer, final ColorProvider colorProvider) { super(id, root, changeRoot, renderer, colorProvider); } @Override - ChangeCoverageRow createRow(final FileNode file, final Locale browserLocale) { - return new ChangeCoverageRow(getOriginalNode(file), file, + ModifiedLinesCoverageRow createRow(final FileNode file, final Locale browserLocale) { + return new ModifiedLinesCoverageRow(getOriginalNode(file), file, browserLocale, getRenderer(), getColorProvider()); } /** * UI row model for the coverage details table of modified lines. */ - private static class ChangeCoverageRow extends ChangesRow { - ChangeCoverageRow(final FileNode originalFile, final FileNode changedFileNode, + private static class ModifiedLinesCoverageRow extends ChangesRow { + ModifiedLinesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { super(originalFile, changedFileNode, browserLocale, renderer, colorProvider); } diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/model/CoverageReporter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/model/CoverageReporter.java index f3b5bef4e..9addc90b1 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/model/CoverageReporter.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/model/CoverageReporter.java @@ -225,7 +225,7 @@ private Optional getReferenceBuildAction(final Run bu coverageBuildAction.getOwner().getDisplayName())); } - if (!previousResult.isPresent()) { + if (previousResult.isEmpty()) { log.logInfo("-> Found no reference result in reference build"); return Optional.empty(); diff --git a/plugin/src/main/resources/coverage/configuration.jelly b/plugin/src/main/resources/coverage/configuration.jelly index ef298486c..94448befa 100644 --- a/plugin/src/main/resources/coverage/configuration.jelly +++ b/plugin/src/main/resources/coverage/configuration.jelly @@ -1,5 +1,5 @@ - + Provides the configuration for the coverage recorder and step. @@ -39,9 +39,6 @@ - - - @@ -52,7 +49,21 @@ + + + + + + + + + + + + + + diff --git a/plugin/src/main/resources/coverage/configuration.properties b/plugin/src/main/resources/coverage/configuration.properties index 565e21f8a..980de9223 100644 --- a/plugin/src/main/resources/coverage/configuration.properties +++ b/plugin/src/main/resources/coverage/configuration.properties @@ -9,6 +9,8 @@ qualityGates.description=You can define an arbitrary number of quality gates tha title.id=Custom ID title.name=Custom Name skipPublishingChecks.title=Skip publishing of checks to SCM hosting platforms +checksName.title=Checks name +checksAnnotationScope.title=Select the scope of source code annotations failOnError.title=Fail the build if errors have been reported during the execution title.enabledForFailure=Enable recording for failed builds title.skipSymbolicLinks=Skip symbolic links when searching for files diff --git a/plugin/src/main/resources/coverage/coverage-table.jelly b/plugin/src/main/resources/coverage/coverage-table.jelly index b8e092bd3..91efd4c6f 100644 --- a/plugin/src/main/resources/coverage/coverage-table.jelly +++ b/plugin/src/main/resources/coverage/coverage-table.jelly @@ -1,11 +1,14 @@ - + Provides a table to render the file coverage nodes without the source code. The ID of the table. + + Determines whether to show the changed files filter toggle. + @@ -15,11 +18,17 @@
+ + +
+ + +
@@ -51,6 +60,9 @@
+ + + diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksAnnotationScope.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksAnnotationScope.html new file mode 100644 index 000000000..737f1ec3f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksAnnotationScope.html @@ -0,0 +1,22 @@ +
+ Select the scope of source code annotations in SCM checks. + + The following different scopes are supported: + +
+
SKIP - Skip annotations
+
+ Do not publish any annotations, just report the coverage report summary. +
+
MODIFIED_LINES - Publish annotations for modified lines
+
+ Publish only annotations for lines that have been changed (with respect to the reference build). + Teams can use these annotations to improve the quality of pull or merge requests. +
+
ALL_LINES - Publish annotations for all lines
+
+ Publish annotations for existing and new code. There might be a lot of annotations depending on + your code coverage. +
+
+
diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksName.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksName.html new file mode 100644 index 000000000..b1d1ace7d --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksName.html @@ -0,0 +1,4 @@ +
+If provided, and publishing checks enabled, the plugin will use this name when publishing results to corresponding +SCM hosting platforms. If not, the default name will be used. +
diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksAnnotationScope.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksAnnotationScope.html new file mode 100644 index 000000000..737f1ec3f --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksAnnotationScope.html @@ -0,0 +1,22 @@ +
+ Select the scope of source code annotations in SCM checks. + + The following different scopes are supported: + +
+
SKIP - Skip annotations
+
+ Do not publish any annotations, just report the coverage report summary. +
+
MODIFIED_LINES - Publish annotations for modified lines
+
+ Publish only annotations for lines that have been changed (with respect to the reference build). + Teams can use these annotations to improve the quality of pull or merge requests. +
+
ALL_LINES - Publish annotations for all lines
+
+ Publish annotations for existing and new code. There might be a lot of annotations depending on + your code coverage. +
+
+
diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksName.html b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksName.html new file mode 100644 index 000000000..b1d1ace7d --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksName.html @@ -0,0 +1,4 @@ +
+If provided, and publishing checks enabled, the plugin will use this name when publishing results to corresponding +SCM hosting platforms. If not, the default name will be used. +
diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.jelly b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.jelly index 3de61eea9..e81b3929d 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.jelly +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel/index.jelly @@ -17,7 +17,7 @@ - +
@@ -33,9 +33,9 @@ - + @@ -72,10 +72,10 @@
- +
- +
diff --git a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties index 83dd1df40..68f0622c7 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties +++ b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties @@ -32,3 +32,14 @@ Column.Complexity=Complexity Column.ComplexityDensity=Complexity / LOC MessagesViewModel.Title=Code Coverage + +Checks.Summary=Coverage Report Overview +Checks.QualityGates=Quality Gates Summary - {0} +Checks.ProjectOverview=Project Coverage Summary +Checks.Annotation.Title=Missing Coverage +Checks.Annotation.Message.SingleLine=Changed line #L{0} is not covered by tests +Checks.Annotation.Message.MultiLine=Changed lines #L{0} - L{1} are not covered by tests + +ChecksAnnotationScope.Skip=Skip annotations +ChecksAnnotationScope.ModifiedLines=Publish annotations for modified lines +ChecksAnnotationScope.AllLines=Publish annotations for all lines diff --git a/plugin/src/main/webapp/js/view-model.js b/plugin/src/main/webapp/js/view-model.js index 384219d5b..fe4300622 100644 --- a/plugin/src/main/webapp/js/view-model.js +++ b/plugin/src/main/webapp/js/view-model.js @@ -470,6 +470,14 @@ const CoverageChartGenerator = function ($) { initializeSourceCodeSelection('absolute-coverage'); initializeSourceCodeSelection('change-coverage'); initializeSourceCodeSelection('indirect-coverage'); + + $('input[name="changed"]').on('change', function () { + const showChanged = $(this).prop('checked'); + $('table.data-table').each(function () { + const table = $(this).DataTable(); + table.column(1).search(showChanged ? 'true' : '').draw(); + }); + }); }); } }; diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverterTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverterTest.java index ab4432c06..795ead83c 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverterTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverterTest.java @@ -1,5 +1,8 @@ package io.jenkins.plugins.coverage.metrics.charts; +import java.util.List; +import java.util.stream.Collectors; + import org.junit.jupiter.api.Test; import edu.hm.hafner.echarts.LabeledTreeMapNode; @@ -11,6 +14,8 @@ import io.jenkins.plugins.coverage.metrics.color.ColorProviderFactory; import io.jenkins.plugins.coverage.metrics.color.CoverageLevel; +import static org.assertj.core.api.Assertions.*; + /** * Tests the class {@link TreeMapNodeConverter}. * @@ -24,44 +29,44 @@ class TreeMapNodeConverterTest extends AbstractCoverageTest { void shouldConvertCodingStyleToTree() { Node tree = readJacocoResult(JACOCO_CODING_STYLE_FILE); - final double totalLines = JACOCO_CODING_STYLE_TOTAL; - final double coveredLines = JACOCO_CODING_STYLE_COVERED; - final double coveredPercentage = coveredLines / totalLines * 100.0; - -// LabeledTreeMapNode root = new TreeMapNodeConverter().toTreeChartModel(tree, Metric.LINE, COLOR_PROVIDER); -// assertThat(root.getName()).isEqualTo("Java coding style"); -// assertThat(root.getValue()).containsExactly(totalLines, coveredLines); -// assertThat(root.getItemStyle().getColor()).isEqualTo(getNodeColorAsRGBHex(coveredPercentage)); -// -// assertThat(root.getChildren()).hasSize(1).element(0).satisfies( -// node -> { -// assertThat(node.getName()).isEqualTo("edu.hm.hafner.util"); -// assertThat(node.getValue()).containsExactly(totalLines, coveredLines); -// assertThat(root.getItemStyle().getColor()).isEqualTo(getNodeColorAsRGBHex(coveredPercentage)); -// } -// ); + LabeledTreeMapNode root = new TreeMapNodeConverter().toTreeChartModel(tree, Metric.LINE, COLOR_PROVIDER); + assertThat(root.getName()).isEqualTo("Java coding style"); + + var overallCoverage = String.valueOf(JACOCO_CODING_STYLE_TOTAL); + assertThat(root.getValue()).contains(overallCoverage); + + var overallCoveragePercentage = 100.0 * JACOCO_CODING_STYLE_COVERED / JACOCO_CODING_STYLE_TOTAL; + assertThat(root.getItemStyle().getColor()).isEqualTo(getNodeColorAsRGBHex(overallCoveragePercentage)); + + assertThat(root.getChildren()).hasSize(1).element(0).satisfies( + node -> { + assertThat(node.getName()).isEqualTo("edu.hm.hafner.util"); + assertThat(node.getValue()).contains(overallCoverage); + assertThat(root.getItemStyle().getColor()).isEqualTo(getNodeColorAsRGBHex(overallCoveragePercentage)); + } + ); } @Test - void shouldConvertAnalysisModelToTree() { + void shouldReadBranchCoverage() { Node tree = readJacocoResult(JACOCO_ANALYSIS_MODEL_FILE); - LabeledTreeMapNode root = new TreeMapNodeConverter().toTreeChartModel(tree, Metric.LINE, COLOR_PROVIDER); + LabeledTreeMapNode root = new TreeMapNodeConverter().toTreeChartModel(tree, Metric.BRANCH, COLOR_PROVIDER); + + var nodes = aggregateChildren(root); + nodes.stream().filter(node -> node.getName().endsWith(".java")).forEach(node -> { + assertThat(node.getValue()).hasSize(2); + }); + } - double totalLines = JACOCO_ANALYSIS_MODEL_TOTAL; - double coveredLines = JACOCO_ANALYSIS_MODEL_COVERED; - double coveredPercentage = coveredLines / totalLines * 100.0; - -// assertThat(root.getName()).isEqualTo("Static Analysis Model and Parsers"); -// assertThat(root.getValue()).containsExactly(totalLines, coveredLines); -// assertThat(root.getItemStyle().getColor()).isEqualTo(getNodeColorAsRGBHex(coveredPercentage)); -// assertThat(root.getChildren()).hasSize(1).element(0).satisfies( -// node -> { -// assertThat(node.getName()).isEqualTo("edu.hm.hafner"); -// assertThat(node.getValue()).containsExactly(totalLines, coveredLines); -// assertThat(node.getItemStyle().getColor()).isEqualTo(getNodeColorAsRGBHex(coveredPercentage)); -// } -// ); + private List aggregateChildren(final LabeledTreeMapNode root) { + var children = root.getChildren(); + var subChildren = children.stream() + .map(this::aggregateChildren) + .flatMap(List::stream) + .collect(Collectors.toList()); + subChildren.addAll(children); + return subChildren; } @Override diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java index df28e8fdb..200e2ce53 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java @@ -21,19 +21,20 @@ */ class SourceCodeFacadeTest extends ResourceTest { private static final String WHOLE_SOURCE_CODE = "SourcecodeTest.html"; - private static final String CHANGE_COVERAGE_SOURCE_CODE = "SourcecodeTestCC.html"; + private static final String MODIFIED_LINES_COVERAGE_SOURCE_CODE = "SourcecodeTestCC.html"; private static final String INDIRECT_COVERAGE_SOURCE_CODE = "SourcecodeTestICC.html"; @Test - void shouldCalculateSourcecodeForChangeCoverage() throws IOException { + void shouldCalculateSourcecodeForModifiedLinesCoverage() throws IOException { SourceCodeFacade sourceCodeFacade = createSourceCodeFacade(); String originalHtml = readHtml(WHOLE_SOURCE_CODE); FileNode node = createFileCoverageNode(); - String requiredHtml = Jsoup.parse(readHtml(CHANGE_COVERAGE_SOURCE_CODE), Parser.xmlParser()).html(); + String requiredHtml = Jsoup.parse(readHtml(MODIFIED_LINES_COVERAGE_SOURCE_CODE), Parser.xmlParser()).html(); - String changeCoverageHtml = sourceCodeFacade.calculateChangeCoverageSourceCode(originalHtml, node); - assertThat(changeCoverageHtml).isEqualTo(requiredHtml); + String modifiedLinesCoverageHtml = + sourceCodeFacade.calculateModifiedLinesCoverageSourceCode(originalHtml, node); + assertThat(modifiedLinesCoverageHtml).isEqualTo(requiredHtml); } @Test @@ -44,8 +45,8 @@ void shouldCalculateSourcecodeForIndirectCoverageChanges() throws IOException { String requiredHtml = Jsoup.parse(readHtml(INDIRECT_COVERAGE_SOURCE_CODE), Parser.xmlParser()).html(); - String changeCoverageHtml = sourceCodeFacade.calculateIndirectCoverageChangesSourceCode(originalHtml, node); - assertThat(changeCoverageHtml).isEqualTo(requiredHtml); + String modifiedLinesCoverageHtml = sourceCodeFacade.calculateIndirectCoverageChangesSourceCode(originalHtml, node); + assertThat(modifiedLinesCoverageHtml).isEqualTo(requiredHtml); } /** @@ -61,7 +62,7 @@ private FileNode createFileCoverageNode() { FileNode file = new FileNode(""); List lines = Arrays.asList(10, 11, 12, 16, 17, 18, 19); for (Integer line : lines) { - file.addModifiedLine(line); + file.addModifiedLines(line); } file.addIndirectCoverageChange(6, -1); file.addIndirectCoverageChange(7, -1); diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildActionTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildActionTest.java index 202ed0f1a..b23c61dda 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildActionTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageBuildActionTest.java @@ -50,7 +50,7 @@ void shouldNotLoadResultIfCoverageValuesArePersistedInAction() { var coverages = List.of(percent50, percent80); var action = spy(new CoverageBuildAction(mock(FreeStyleBuild.class), CoverageRecorder.DEFAULT_ID, StringUtils.EMPTY, StringUtils.EMPTY, module, new QualityGateResult(), - createLog(), "-", deltas, coverages, deltas, coverages, false)); + createLog(), "-", deltas, coverages, deltas, coverages, deltas, coverages, false)); when(action.getResult()).thenThrow(new IllegalStateException("Result should not be accessed with getResult() when getting a coverage metric that is persisted in the build")); @@ -60,6 +60,8 @@ StringUtils.EMPTY, StringUtils.EMPTY, module, new QualityGateResult(), assertThat(action.getStatistics().getValue(Baseline.PROJECT, Metric.LINE)).hasValue(percent80); assertThat(action.getStatistics().getValue(Baseline.MODIFIED_LINES, Metric.BRANCH)).hasValue(percent50); assertThat(action.getStatistics().getValue(Baseline.MODIFIED_LINES, Metric.LINE)).hasValue(percent80); + assertThat(action.getStatistics().getValue(Baseline.MODIFIED_FILES, Metric.BRANCH)).hasValue(percent50); + assertThat(action.getStatistics().getValue(Baseline.MODIFIED_FILES, Metric.LINE)).hasValue(percent80); assertThat(action.getStatistics().getValue(Baseline.PROJECT_DELTA, Metric.LINE)) .hasValue(new FractionValue(Metric.LINE, lineDelta)); assertThat(action.getStatistics().getValue(Baseline.PROJECT_DELTA, Metric.BRANCH)) @@ -71,7 +73,7 @@ StringUtils.EMPTY, StringUtils.EMPTY, module, new QualityGateResult(), private static CoverageBuildAction createEmptyAction(final Node module) { return new CoverageBuildAction(mock(FreeStyleBuild.class), CoverageRecorder.DEFAULT_ID, StringUtils.EMPTY, StringUtils.EMPTY, module, new QualityGateResult(), createLog(), "-", - new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), false); + new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), false); } private static FilteredLog createLog() { diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java new file mode 100644 index 000000000..20d5b419d --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java @@ -0,0 +1,133 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.Fraction; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junitpioneer.jupiter.DefaultLocale; + +import edu.hm.hafner.metric.Coverage.CoverageBuilder; +import edu.hm.hafner.metric.Metric; + +import hudson.model.Run; + +import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationLevel; +import io.jenkins.plugins.checks.api.ChecksConclusion; +import io.jenkins.plugins.checks.api.ChecksDetails; +import io.jenkins.plugins.checks.api.ChecksOutput; +import io.jenkins.plugins.checks.api.ChecksStatus; +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; +import io.jenkins.plugins.coverage.metrics.steps.CoverageRecorder.ChecksAnnotationScope; +import io.jenkins.plugins.util.JenkinsFacade; +import io.jenkins.plugins.util.QualityGateResult; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +@DefaultLocale("en") +class CoverageChecksPublisherTest extends AbstractCoverageTest { + private static final String JENKINS_BASE_URL = "http://127.0.0.1:8080"; + private static final String BUILD_LINK = "job/pipeline-coding-style/job/5"; + private static final String COVERAGE_ID = "coverage"; + private static final String REPORT_NAME = "Name"; + + @ParameterizedTest(name = "should create checks (scope = {0}, expected annotations = {1})") + @CsvSource({"SKIP, 0", "ALL_LINES, 36", "MODIFIED_LINES, 3"}) + void shouldCreateChecksReport(final ChecksAnnotationScope scope, final int expectedAnnotations) { + var publisher = new CoverageChecksPublisher(createCoverageBuildAction(), REPORT_NAME, scope, createJenkins()); + + var checkDetails = publisher.extractChecksDetails(); + + assertThat(checkDetails.getName()).isPresent().get().isEqualTo(REPORT_NAME); + assertThat(checkDetails.getStatus()).isEqualTo(ChecksStatus.COMPLETED); + assertThat(checkDetails.getConclusion()).isEqualTo(ChecksConclusion.SUCCESS); + assertThat(checkDetails.getDetailsURL()).isPresent() + .get() + .isEqualTo("http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage"); + assertThatDetailsAreCorrect(checkDetails, expectedAnnotations); + } + + private void assertThatDetailsAreCorrect(final ChecksDetails checkDetails, final int expectedAnnotations) { + assertThat(checkDetails.getOutput()).isPresent().get().satisfies(output -> { + assertThat(output.getTitle()).isPresent() + .get() + .isEqualTo("Modified code lines: 50.00% (1/2)"); + assertThat(output.getText()).isEmpty(); + assertChecksAnnotations(output, expectedAnnotations); + assertSummary(output); + }); + } + + private void assertSummary(final ChecksOutput checksOutput) throws IOException { + var expectedContent = Files.readString(getResourceAsFile("coverage-publisher-summary.md")); + assertThat(checksOutput.getSummary()).isPresent() + .get() + .isEqualTo(expectedContent); + } + + private void assertChecksAnnotations(final ChecksOutput checksOutput, final int expectedAnnotations) { + if (expectedAnnotations == 3) { + assertThat(checksOutput.getChecksAnnotations()).hasSize(expectedAnnotations).satisfiesExactly( + annotation -> { + assertThat(annotation.getTitle()).contains("Not covered line"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).contains("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).contains("Line 61 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(61); + }, + annotation -> { + assertThat(annotation.getTitle()).contains("Not covered line"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).contains("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).contains("Line 62 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(62); + }, + annotation -> { + assertThat(annotation.getTitle()).contains("Partially covered line"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).contains("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).contains("Line 113 is only partially covered, one branch is missing"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(113); + }); + } + else { + assertThat(checksOutput.getChecksAnnotations()).hasSize(expectedAnnotations); + } + } + + private JenkinsFacade createJenkins() { + JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class); + when(jenkinsFacade.getAbsoluteUrl(BUILD_LINK, COVERAGE_ID)).thenReturn( + JENKINS_BASE_URL + "/" + BUILD_LINK + "/" + COVERAGE_ID); + return jenkinsFacade; + } + + private CoverageBuildAction createCoverageBuildAction() { + var testCoverage = new CoverageBuilder().setMetric(Metric.LINE) + .setCovered(1) + .setMissed(1) + .build(); + + var run = mock(Run.class); + when(run.getUrl()).thenReturn(BUILD_LINK); + var result = readJacocoResult("jacoco-codingstyle.xml"); + result.findFile("TreeStringBuilder.java") + .ifPresent(file -> { + assertThat(file.getMissedLines()).contains(61, 62); + assertThat(file.getPartiallyCoveredLines()).contains(entry(113, 1)); + file.addModifiedLines(61, 62, 113); + }); + + return new CoverageBuildAction(run, COVERAGE_ID, REPORT_NAME, StringUtils.EMPTY, result, + new QualityGateResult(), null, "refId", + new TreeMap<>(Map.of(Metric.LINE, Fraction.ONE_HALF, Metric.MODULE, Fraction.ONE_FIFTH)), + List.of(testCoverage), new TreeMap<>(Map.of(Metric.LINE, Fraction.ONE_HALF)), List.of(testCoverage), + new TreeMap<>(Map.of(Metric.LINE, Fraction.ONE_HALF)), List.of(testCoverage), false); + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumnTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumnTest.java index 123534ad3..0b0b5b29c 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumnTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageMetricColumnTest.java @@ -104,11 +104,19 @@ void shouldProvideSelectedColumn() { column.setBaseline(Baseline.MODIFIED_LINES); assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_LINES); - assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#changeCoverage"); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedLinesCoverage"); column.setBaseline(Baseline.MODIFIED_LINES_DELTA); assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_LINES_DELTA); - assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#changeCoverage"); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedLinesCoverage"); + + column.setBaseline(Baseline.MODIFIED_FILES); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_FILES); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedFilesCoverage"); + + column.setBaseline(Baseline.MODIFIED_FILES_DELTA); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_FILES_DELTA); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedFilesCoverage"); column.setBaseline(Baseline.INDIRECT); assertThat(column.getBaseline()).isEqualTo(Baseline.INDIRECT); @@ -219,7 +227,7 @@ private CoverageMetricColumn createColumn() { CoverageBuildAction coverageBuildAction = new CoverageBuildAction(run, "coverage", "Code Coverage", StringUtils.EMPTY, node, new QualityGateResult(), new FilteredLog("Test"), - "-", delta, List.of(), new TreeMap<>(), List.of(), false); + "-", delta, List.of(), new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), false); when(run.getAction(CoverageBuildAction.class)).thenReturn(coverageBuildAction); when(run.getActions(CoverageBuildAction.class)).thenReturn(Collections.singletonList(coverageBuildAction)); diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModelTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModelTest.java index 283ed1e46..e19c19951 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModelTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModelTest.java @@ -35,7 +35,7 @@ void shouldReturnEmptySourceViewForExistingLinkButMissingSourceFile() { String hash = String.valueOf("PathUtil.java".hashCode()); assertThat(model.getSourceCode(hash, ABSOLUTE_COVERAGE_TABLE_ID)).isEqualTo("n/a"); - assertThat(model.getSourceCode(hash, CHANGE_COVERAGE_TABLE_ID)).isEqualTo("n/a"); + assertThat(model.getSourceCode(hash, MODIFIED_LINES_COVERAGE_TABLE_ID)).isEqualTo("n/a"); assertThat(model.getSourceCode(hash, INDIRECT_COVERAGE_TABLE_ID)).isEqualTo("n/a"); } @@ -92,7 +92,7 @@ private Node createIndirectCoverageChangesNode() { @Test void shouldProvideRightTableModelById() { CoverageViewModel model = createModelFromCodingStyleReport(); - assertThat(model.getTableModel(CHANGE_COVERAGE_TABLE_ID)).isInstanceOf(ChangeCoverageTableModel.class); + assertThat(model.getTableModel(MODIFIED_LINES_COVERAGE_TABLE_ID)).isInstanceOf(ModifiedLinesCoverageTableModel.class); assertThat(model.getTableModel(INDIRECT_COVERAGE_TABLE_ID)).isInstanceOf(IndirectCoverageChangesTable.class); assertThat(model.getTableModel(ABSOLUTE_COVERAGE_TABLE_ID)).isInstanceOf(CoverageTableModel.class); diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStreamTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStreamTest.java index 57978a501..accd11557 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStreamTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStreamTest.java @@ -230,7 +230,7 @@ CoverageBuildAction createAction() { return new CoverageBuildAction(mock(FreeStyleBuild.class), CoverageRecorder.DEFAULT_ID, StringUtils.EMPTY, StringUtils.EMPTY, tree, new QualityGateResult(), new FilteredLog("Test"), "-", - new TreeMap<>(), List.of(), + new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), new TreeMap<>(), List.of(), false); } diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/DeltaComputationITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/DeltaComputationITest.java index a18b81f6f..c95573f00 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/DeltaComputationITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/DeltaComputationITest.java @@ -115,19 +115,19 @@ private void verifyDeltaComputation(final Run firstBuild, final Run assertThat(action.formatDelta(Baseline.PROJECT, LOC)).isEqualTo(String.valueOf(-JACOCO_ANALYSIS_MODEL_TOTAL)); assertThat(action.formatDelta(Baseline.PROJECT, COMPLEXITY)).isEqualTo(String.valueOf(160 - 2718)); - verifyChangeCoverage(action); + verifyModifiedLinesCoverage(action); } /** - * Verifies the calculated change coverage including the change coverage delta and the code delta. This makes sure + * Verifies the calculated modified lines coverage including the modified lines coverage delta and the code delta. This makes sure * these metrics are set properly even if there are no code changes. * * @param action * The created Jenkins action */ - private void verifyChangeCoverage(final CoverageBuildAction action) { + private void verifyModifiedLinesCoverage(final CoverageBuildAction action) { Node root = action.getResult(); assertThat(root).isNotNull(); - assertThat(root.getAllFileNodes()).flatExtracting(FileNode::getChangedLines).isEmpty(); + assertThat(root.getAllFileNodes()).flatExtracting(FileNode::getModifiedLines).isEmpty(); } } diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessorTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessorTest.java index c74c9641c..f6688e30e 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessorTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/FileChangesProcessorTest.java @@ -86,13 +86,13 @@ void shouldAttachChangesCodeLines() { assertThat(tree.findByHashCode(Metric.FILE, TEST_FILE_1_PATH.hashCode())) .isNotEmpty() .satisfies(node -> assertThat(node.get()) - .isInstanceOfSatisfying(FileNode.class, f -> assertThat(f.getChangedLines()) + .isInstanceOfSatisfying(FileNode.class, f -> assertThat(f.getModifiedLines()) .containsExactly( 5, 6, 7, 8, 9, 14, 15, 16, 17, 18, 20, 21, 22, 33, 34, 35, 36))); assertThat(tree.findByHashCode(Metric.FILE, TEST_FILE_2.hashCode())) .isNotEmpty() .satisfies(node -> assertThat(node.get()) - .isInstanceOfSatisfying(FileNode.class, f -> assertThat(f.getChangedLines()) + .isInstanceOfSatisfying(FileNode.class, f -> assertThat(f.getModifiedLines()) .isEmpty())); } diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/GitForensicsITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/GitForensicsITest.java index 2c27c2acb..c24ad30ad 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/GitForensicsITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/GitForensicsITest.java @@ -154,7 +154,7 @@ private void verifyGitIntegration(final Run build, final Run referen */ private void verifyCoverage(final CoverageBuildAction action) { verifyOverallCoverage(action); - verifyChangeCoverage(action); + verifyModifiedLinesCoverage(action); verifyIndirectCoverageChanges(action); } @@ -172,12 +172,12 @@ private void verifyOverallCoverage(final CoverageBuildAction action) { } /** - * Verifies the calculated change coverage including the change coverage delta. + * Verifies the calculated modified lines coverage including the modified lines coverage delta. * * @param action * The created Jenkins action */ - private void verifyChangeCoverage(final CoverageBuildAction action) { + private void verifyModifiedLinesCoverage(final CoverageBuildAction action) { var builder = new CoverageBuilder(); assertThat(action.getAllValues(Baseline.MODIFIED_LINES)).contains( builder.setMetric(LINE).setCovered(1).setMissed(1).build()); @@ -206,14 +206,14 @@ private void verifyCodeDelta(final CoverageBuildAction action) { edu.hm.hafner.metric.Node root = action.getResult(); assertThat(root).isNotNull(); - List changedFiles = root.getAllFileNodes().stream() - .filter(FileNode::hasChangedLines) + List modifiedFiles = root.getAllFileNodes().stream() + .filter(FileNode::hasModifiedLines) .collect(Collectors.toList()); - assertThat(changedFiles).hasSize(4); - assertThat(changedFiles).extracting(FileNode::getName) + assertThat(modifiedFiles).hasSize(4); + assertThat(modifiedFiles).extracting(FileNode::getName) .containsExactlyInAnyOrder("MinerFactory.java", "RepositoryMinerStep.java", "SimpleReferenceRecorder.java", "CommitDecoratorFactory.java"); - assertThat(changedFiles).flatExtracting(FileNode::getChangedLines) + assertThat(modifiedFiles).flatExtracting(FileNode::getModifiedLines) .containsExactlyInAnyOrder(15, 17, 63, 68, 80, 90, 130); } diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.md b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.md new file mode 100644 index 000000000..09f7a2f97 --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.md @@ -0,0 +1,32 @@ +## Coverage Report Overview + +* **[Overall project (difference to reference job)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#overview)** + * Line Coverage: 91.02% (294/323) / +50.00% + * Branch Coverage: 93.97% (109/116) / n/a + * Complexity Density: +49.54% + * Lines of Code: 323 +* **[Modified files (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#modifiedFilesCoverage)** + * Line Coverage: 50.00% (1/2) / +50.00% + * Branch Coverage: n/a / n/a + * Complexity Density: +43.40% + * Lines of Code: 53 +* **[Modified code lines (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#modifiedLinesCoverage)** + * Line Coverage: 50.00% (1/2) / +50.00% + * Branch Coverage: n/a / n/a + * Lines of Code: 3 +* **[Indirect changes](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#indirectCoverage)** + * Line Coverage: 50.00% (1/2) / n/a + * Branch Coverage: n/a / n/a + * Lines of Code: n/a + + +## Quality Gates Summary - INACTIVE + + + +## Project Coverage Summary + +|Container Coverage|Module Coverage|Package Coverage|File Coverage|Class Coverage|Method Coverage|Line Coverage|Branch Coverage|Instruction Coverage| +|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +|:white_check_mark: **Overall project**|100.00% (1/1)|100.00% (4/4)|70.00% (7/10)|83.33% (15/18)|95.10% (97/102)|91.02% (294/323)|93.97% (109/116)|93.33% (1260/1350)| +|:chart_with_upwards_trend: **Overall project (difference to reference job)**|-|+20.00% :arrow_up:|-|-|-|-|+50.00% :arrow_up:|-|-|