From fe45ac24001a82267148de66a12886090c77b097 Mon Sep 17 00:00:00 2001 From: Florian Orendi Date: Sun, 5 Feb 2023 18:37:11 +0100 Subject: [PATCH 01/23] Add Modified Files Coverage and GitHub Checks API --- .../coverage/metrics/model/Baseline.java | 4 +- .../metrics/model/ElementFormatter.java | 51 +++ .../steps/ChangedFilesCoverageTable.java | 109 +++++ .../metrics/steps/CoverageBuildAction.java | 57 ++- .../steps/CoverageChecksPublisher.java | 416 ++++++++++++++++++ .../metrics/steps/CoveragePercentage.java | 2 +- .../metrics/steps/CoverageRecorder.java | 11 + .../metrics/steps/CoverageReporter.java | 10 + .../metrics/steps/CoverageTableModel.java | 6 +- .../metrics/steps/CoverageViewModel.java | 18 +- .../coverage/model/CoverageReporter.java | 2 +- .../metrics/steps/Messages.properties | 7 + .../steps/CoverageBuildActionTest.java | 6 +- .../steps/CoverageChecksPublisherTest.java | 125 ++++++ .../steps/CoverageMetricColumnTest.java | 10 +- .../metrics/steps/CoveragePercentageTest.java | 2 +- .../metrics/steps/CoverageXmlStreamTest.java | 6 +- .../steps/coverage-publisher-summary.MD | 32 ++ 18 files changed, 846 insertions(+), 28 deletions(-) create mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java create mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java create mode 100644 plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java create mode 100644 plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.MD 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..0eff1eee3 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 @@ -38,12 +38,12 @@ public enum Baseline { * 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(), "changedFilesCoverage", 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(), "changedFilesCoverage", 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..fc73491b4 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,13 @@ package io.jenkins.plugins.coverage.metrics.model; +import java.util.Comparator; +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 +16,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; @@ -370,6 +376,51 @@ 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() + // use the default comparator which uses the enum ordinal + .sorted() + .map(this::getDisplayName) + .collect(Collectors.toList()); + } + + /** + * Formats a stream of values to theis display representation by using the passed 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) + .sorted(Comparator.comparing(Coverage::getMetric)); + } + /** * Returns a localized human-readable label for the specified metric. * diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java new file mode 100644 index 000000000..ee6b707af --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java @@ -0,0 +1,109 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +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 hudson.Functions; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.datatables.DetailedCell; +import io.jenkins.plugins.datatables.TableConfiguration; +import io.jenkins.plugins.datatables.TableConfiguration.SelectStyle; + +/** + * {@link CoverageTableModel} implementation for visualizing the changed files coverage. + * + * @since 4.0.0 + */ +class ChangedFilesCoverageTable extends CoverageTableModel { + private final Node changedFilesCoverageRoot; + + /** + * Creates a changed files coverage table model. + * + * @param id + * The ID of the table + * @param root + * The root of the origin coverage tree + * @param changeRoot + * The root of the change coverage tree + * @param renderer + * the renderer to use for the file names + * @param colorProvider + * The {@link ColorProvider} which provides the used colors + */ + ChangedFilesCoverageTable(final String id, final Node root, final Node changeRoot, + final RowRenderer renderer, final ColorProvider colorProvider) { + super(id, root, renderer, colorProvider); + + this.changedFilesCoverageRoot = changeRoot; + } + + @Override + public TableConfiguration getTableConfiguration() { + return super.getTableConfiguration().select(SelectStyle.SINGLE); + } + + @Override + public List getRows() { + Locale browserLocale = Functions.getCurrentLocale(); + return changedFilesCoverageRoot.getAllFileNodes().stream() + .map(file -> new ChangedFilesCoverageRow( + getOriginalNode(file), file, browserLocale, getRenderer(), getColorProvider())) + .collect(Collectors.toList()); + } + + private FileNode getOriginalNode(final FileNode fileNode) { + return getRoot().getAllFileNodes().stream() + .filter(node -> node.getPath().equals(fileNode.getPath()) + && node.getName().equals(fileNode.getName())) + .findFirst() + .orElse(fileNode); // return this as fallback to prevent exceptions + } + + /** + * UI row model for the changed files coverage details table. + * + * @since 4.0.0 + */ + private static class ChangedFilesCoverageRow extends CoverageRow { + private final FileNode originalFile; + + ChangedFilesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, + final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { + super(changedFileNode, browserLocale, renderer, colorProvider); + + this.originalFile = originalFile; + } + + @Override + public DetailedCell getLineCoverageDelta() { + return createColoredChangeCoverageDeltaColumn(Metric.LINE); + } + + @Override + public DetailedCell getBranchCoverageDelta() { + return createColoredChangeCoverageDeltaColumn(Metric.BRANCH); + } + + @Override + public int getLoc() { + return getFile().getCoveredLines().size(); + } + + private DetailedCell createColoredChangeCoverageDeltaColumn(final Metric metric) { + Coverage fileCoverage = getFile().getTypedValue(metric, Coverage.nullObject(metric)); + if (fileCoverage.isSet()) { + return createColoredCoverageDeltaColumn(metric, + fileCoverage.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..abea0119b 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 @@ -80,6 +80,12 @@ public final class CoverageBuildAction extends BuildAction implements Stap /** The delta of the coverages of the associated change request with respect to the reference build. */ private final NavigableMap changeCoverageDifference; + /** The coverage of the changed files. */ + private final List changedFilesCoverage; + + /** The coverage delta of the changed files. */ + private final NavigableMap changedFilesCoverageDifference; + /** The indirect coverage changes of the associated change request with respect to the reference build. */ private final List indirectCoverageChanges; @@ -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()); } /** @@ -150,9 +156,12 @@ public CoverageBuildAction(final Run owner, final String id, final String final NavigableMap delta, final List changeCoverage, final NavigableMap changeCoverageDifference, + final List changedFilesCoverage, + final NavigableMap changedFilesCoverageDifference, final List indirectCoverageChanges) { this(owner, id, optionalName, icon, result, qualityGateResult, log, referenceBuildId, delta, changeCoverage, - changeCoverageDifference, indirectCoverageChanges, true); + changeCoverageDifference, changedFilesCoverage, changedFilesCoverageDifference, indirectCoverageChanges, + true); } @VisibleForTesting @@ -163,6 +172,8 @@ public CoverageBuildAction(final Run owner, final String id, final String final NavigableMap delta, final List changeCoverage, final NavigableMap changeCoverageDifference, + final List changedFilesCoverage, + final NavigableMap changedFilesCoverageDifference, final List indirectCoverageChanges, final boolean canSerialize) { super(owner, result, false); @@ -177,6 +188,8 @@ public CoverageBuildAction(final Run owner, final String id, final String difference = delta; this.changeCoverage = new ArrayList<>(changeCoverage); this.changeCoverageDifference = changeCoverageDifference; + this.changedFilesCoverage = new ArrayList<>(changedFilesCoverage); + this.changedFilesCoverageDifference = changedFilesCoverageDifference; this.indirectCoverageChanges = new ArrayList<>(indirectCoverageChanges); this.referenceBuildId = referenceBuildId; @@ -208,7 +221,7 @@ public ElementFormatter getFormatter() { public CoverageStatistics getStatistics() { return new CoverageStatistics(projectValues, difference, changeCoverage, changeCoverageDifference, - List.of(), new TreeMap<>()); + changedFilesCoverage, changedFilesCoverageDifference); } /** @@ -218,7 +231,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 +286,18 @@ 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 changeCoverageDifference; + } + else if (deltaBaseline == Baseline.MODIFIED_FILES_DELTA) { + return changedFilesCoverageDifference; + } + throw new NoSuchElementException("No delta baseline: " + deltaBaseline); + } + /** * Returns all important values for the specified baseline. * @@ -300,12 +325,23 @@ private Stream getValueStream(final Baseline baseline) { if (baseline == Baseline.MODIFIED_LINES) { return changeCoverage.stream(); } + if (baseline == Baseline.MODIFIED_FILES) { + return changedFilesCoverage.stream(); + } if (baseline == Baseline.INDIRECT) { return indirectCoverageChanges.stream(); } throw new NoSuchElementException("No such baseline: " + baseline); } + public String formatValueForMetric(final Baseline baseline, final Metric metric) { + var valueOfMetric = getAllValues(baseline).stream() + .filter(value -> value.getMetric().equals(metric)) + .findFirst(); + return valueOfMetric.map(this::formatValueWithMetric) + .orElseGet(() -> FORMATTER.getDisplayName(metric) + ": " + io.jenkins.plugins.coverage.metrics.steps.Messages.Coverage_Not_Available()); + } + /** * Returns whether a delta metric for the specified baseline exists. * @@ -316,7 +352,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; } /** @@ -337,6 +374,10 @@ public boolean hasDelta(final Baseline baseline, final Metric metric) { return changeCoverageDifference.containsKey(metric) && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); } + if (baseline == Baseline.MODIFIED_FILES) { + return changedFilesCoverageDifference.containsKey(metric) + && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); + } if (baseline == Baseline.INDIRECT) { return false; } @@ -368,6 +409,12 @@ public String formatDelta(final Baseline baseline, final Metric metric) { Functions.getCurrentLocale()); } } + if (baseline == Baseline.MODIFIED_FILES) { + if (hasDelta(baseline, metric)) { + return FORMATTER.formatDelta(changedFilesCoverageDifference.get(metric), metric, + Functions.getCurrentLocale()); + } + } return Messages.Coverage_Not_Available(); } 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..8c6963199 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java @@ -0,0 +1,416 @@ +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.function.BiFunction; + +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.util.JenkinsFacade; +import io.jenkins.plugins.util.QualityGateStatus; + +/** + * Publishes coverage as Checks to SCM platforms. + * + * @author Florian Orendi + */ +class CoverageChecksPublisher { + + private final ElementFormatter formatter; + + private final CoverageBuildAction action; + private final JenkinsFacade jenkinsFacade; + private final String checksName; + + CoverageChecksPublisher(final CoverageBuildAction action, final String checksName) { + this(action, checksName, new JenkinsFacade()); + } + + @VisibleForTesting + CoverageChecksPublisher(final CoverageBuildAction action, + final String checksName, final JenkinsFacade jenkinsFacade) { + this.jenkinsFacade = jenkinsFacade; + this.action = action; + this.checksName = checksName; + this.formatter = new ElementFormatter(); + } + + void publishChecks(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("Change Coverage: %s (%s)", + action.formatValueForMetric(Baseline.MODIFIED_LINES, Metric.LINE), + formatValueOfMetric(action.getResult(), Metric.LOC)); + } + + private String getSummary() { + var root = action.getResult(); + return getOverallCoverageSummary(root) + "\n\n" + + getHealthReportSummary() + "\n\n" + + getProjectMetricsSummary(root); + } + + private List getAnnotations() { + var annotations = new ArrayList(); + for (var fileNode : action.getResult().filterChanges().getAllFileNodes()) { + for (var aggregatedLines : getAggregatedMissingLines(fileNode)) { + ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder() + .withPath(fileNode.getPath()) + .withTitle(Messages.Checks_Annotation_Title()) + .withAnnotationLevel(ChecksAnnotationLevel.WARNING) + .withMessage(getAnnotationMessage(aggregatedLines)) + .withStartLine(aggregatedLines.startLine) + .withEndLine(aggregatedLines.endLine); + + annotations.add(builder.build()); + } + } + + return annotations; + } + + private String getAnnotationMessage(final AggregatedMissingLines aggregatedMissingLines) { + if (aggregatedMissingLines.startLine == aggregatedMissingLines.endLine) { + return Messages.Checks_Annotation_Message_SingleLine(aggregatedMissingLines.startLine); + } + return Messages.Checks_Annotation_Message_MultiLine(aggregatedMissingLines.startLine, + aggregatedMissingLines.endLine); + } + + private List getAggregatedMissingLines(final FileNode fileNode) { + var aggregatedMissingLines = new ArrayList(); + + if (fileNode.hasCoveredLinesInChangeSet()) { + var linesWithCoverage = fileNode.getCoveredLines(); + // there has to be at least one line when it is a file node with changes + var previousLine = linesWithCoverage.first(); + var aggregatedLines = new AggregatedMissingLines(previousLine); + linesWithCoverage.remove(previousLine); + + for (final var line : linesWithCoverage) { + if (line == previousLine + 1) { + aggregatedLines.increaseEndLine(); + } + else { + aggregatedMissingLines.add(aggregatedLines); + aggregatedLines = new AggregatedMissingLines(line); + } + previousLine = line; + } + + aggregatedMissingLines.add(aggregatedLines); + } + + return aggregatedMissingLines; + } + + 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 changedFilesCoverageRoot = root.filterByChangedFilesCoverage(); + var changeCoverageRoot = root.filterChanges(); + var indirectlyChangedCoverage = root.filterByIndirectlyChangedCoverage(); + + var projectCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.PROJECT_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.PROJECT_DELTA.getUrl()))); + var changedFilesCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.MODIFIED_FILES_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.MODIFIED_FILES_DELTA.getUrl()))); + var changeCoverageHeader = 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, action::formatValueForMetric, Baseline.PROJECT)); + var projectCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.PROJECT)); + var projectCoverageComplexity = getBulletListItem(2, formatValueOfMetric(root, Metric.COMPLEXITY_DENSITY)); + var projectCoverageLoc = getBulletListItem(2, formatValueOfMetric(root, Metric.LOC)); + + var changedFilesCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, action::formatValueForMetric, Baseline.MODIFIED_FILES)); + var changedFilesCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.MODIFIED_FILES)); + var changedFilesCoverageComplexity = getBulletListItem(2, + formatValueOfMetric(changedFilesCoverageRoot, Metric.COMPLEXITY_DENSITY)); + var changedFilesCoverageLoc = getBulletListItem(2, + formatValueOfMetric(changedFilesCoverageRoot, Metric.LOC)); + + var changeCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, action::formatValueForMetric, Baseline.MODIFIED_LINES)); + var changeCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.MODIFIED_LINES)); + var changeCoverageLoc = getBulletListItem(2, formatValueOfMetric(changeCoverageRoot, Metric.LOC)); + + var indirectCoverageChangesLine = getBulletListItem(2, + action.formatValueForMetric(Baseline.INDIRECT, Metric.LINE)); + var indirectCoverageChangesBranch = getBulletListItem(2, + action.formatValueForMetric(Baseline.INDIRECT, Metric.BRANCH)); + var indirectCoverageChangesLoc = getBulletListItem(2, + formatValueOfMetric(indirectlyChangedCoverage, Metric.LOC)); + + return sectionHeader + + projectCoverageHeader + + projectCoverageLine + + projectCoverageBranch + + projectCoverageComplexity + + projectCoverageLoc + + changedFilesCoverageHeader + + changedFilesCoverageLine + + changedFilesCoverageBranch + + changedFilesCoverageComplexity + + changedFilesCoverageLoc + + changeCoverageHeader + + changeCoverageLine + + changeCoverageBranch + + changeCoverageLoc + + 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 getHealthReportSummary() { + return getSectionHeader(2, Messages.Checks_QualityGates(action.getQualityGateResult().toString())); + } + + 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 BiFunction coverageFormat, + final Baseline baseline) { + return String.format("%s (%s)", + coverageFormat.apply(baseline, metric), action.formatDelta(baseline, metric)); + } + + private String formatValueOfMetric(final Node root, final Metric metric) { + var value = root.getValue(metric); + return value.map(action::formatValueWithDetails) + .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 + } + + private static class AggregatedMissingLines { + private final int startLine; + private int endLine; + + private AggregatedMissingLines(final int startLine) { + this.startLine = startLine; + this.endLine = startLine; + } + + private void increaseEndLine() { + endLine++; + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java index f0dad34fd..4fadc518d 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java @@ -147,7 +147,7 @@ public String formatPercentage(final Locale locale) { * @return the formatted delta percentage as plain text with a leading sign */ public String formatDeltaPercentage(final Locale locale) { - return String.format(locale, "%+.2f%%", getDoubleValue()); + return String.format(locale, "%+.2f", getDoubleValue()); } public int getNumerator() { 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..8a32f223a 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,9 @@ */ @SuppressWarnings("checkstyle:ClassFanOutComplexity") public class CoverageRecorder extends Recorder { + // TODO: make customizable? + private 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. */ @@ -353,6 +356,14 @@ void perform(final Run run, final FilePath workspace, final TaskListener t getSourceCodeEncoding(), getSourceCodeRetention(), resultHandler); } } + + if (!skipPublishingChecks) { + var coverageAction = run.getAction(CoverageBuildAction.class); + if (coverageAction != null) { + var checksPublisher = new CoverageChecksPublisher(coverageAction, CHECKS_DEFAULT_NAME); + checksPublisher.publishChecks(taskListener); + } + } } else { logHandler.log("Skipping execution of coverage recorder since overall result is '%s'", overallResult); 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..7f0e47255 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; @@ -62,11 +63,18 @@ void publishAction(final String id, final String optionalName, final String icon Node changeCoverageRoot = rootNode.filterChanges(); NavigableMap changeCoverageDelta; + List aggregatedChangedFilesCoverage; + NavigableMap changedFilesCoverageDelta; if (hasChangeCoverage(changeCoverageRoot)) { changeCoverageDelta = changeCoverageRoot.computeDelta(rootNode); + Node changedFilesCoverageRoot = rootNode.filterByChangedFilesCoverage(); + aggregatedChangedFilesCoverage = changedFilesCoverageRoot.aggregateValues(); + changedFilesCoverageDelta = changedFilesCoverageRoot.computeDelta(rootNode); } else { changeCoverageDelta = new TreeMap<>(); + aggregatedChangedFilesCoverage = new ArrayList<>(); + changedFilesCoverageDelta = new TreeMap<>(); if (rootNode.hasChangedLines()) { log.logInfo("No detected code changes affect the code coverage"); } @@ -85,6 +93,8 @@ void publishAction(final String id, final String optionalName, final String icon coverageDelta, changeCoverageRoot.aggregateValues(), changeCoverageDelta, + aggregatedChangedFilesCoverage, + changedFilesCoverageDelta, indirectCoverageChangesTree.aggregateValues()); if (sourceCodeRetention == SourceCodeRetention.MODIFIED) { filesToStore = changeCoverageRoot.getAllFileNodes(); 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..7851af459 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 @@ -115,7 +115,7 @@ public List getColumns() { .withType(ColumnType.NUMBER) .build(); columns.add(loc); - if (root.containsMetric(Metric.COMPLEXITY)) { + if (root.getMetrics().contains(Metric.COMPLEXITY)) { TableColumn complexity = new ColumnBuilder().withHeaderLabel(Messages.Column_Complexity()) .withDataPropertyKey("complexity") .withResponsivePriority(500) @@ -123,7 +123,7 @@ public List getColumns() { .build(); columns.add(complexity); } - if (root.containsMetric(Metric.COMPLEXITY_DENSITY)) { + if (root.getMetrics().contains(Metric.COMPLEXITY_DENSITY)) { TableColumn complexity = new ColumnBuilder().withHeaderLabel(Messages.Column_ComplexityDensity()) .withDataPropertyKey("density") .withDetailedCell() @@ -137,7 +137,7 @@ public List getColumns() { private void configureValueColumn(final String key, final Metric metric, final String headerLabel, final String deltaHeaderLabel, final List columns) { - if (root.containsMetric(metric)) { + if (root.getMetrics().contains(metric)) { TableColumn lineCoverage = new ColumnBuilder().withHeaderLabel(headerLabel) .withDataPropertyKey(key) .withDetailedCell() 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..5788b6cb0 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; @@ -69,6 +68,8 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen static final String ABSOLUTE_COVERAGE_TABLE_ID = "absolute-coverage-table"; static final String CHANGE_COVERAGE_TABLE_ID = "change-coverage-table"; + + static final String CHANGED_FILES_COVERAGE_TABLE_ID = "changed-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"; @@ -85,6 +86,7 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen private final String id; private final Node changeCoverageTreeRoot; + private final Node changedFilesCoverageTreeRoot; private final Node indirectCoverageChangesTreeRoot; private final Function trendChartFunction; @@ -110,6 +112,7 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen // initialize filtered coverage trees so that they will not be calculated multiple times changeCoverageTreeRoot = node.filterChanges(); + changedFilesCoverageTreeRoot = node.filterByChangedFilesCoverage(); indirectCoverageChangesTreeRoot = node.filterByIndirectlyChangedCoverage(); this.trendChartFunction = trendChartFunction; } @@ -294,6 +297,9 @@ public TableModel getTableModel(final String tableId) { case CHANGE_COVERAGE_TABLE_ID: return new ChangeCoverageTableModel(tableId, getNode(), changeCoverageTreeRoot, renderer, colorProvider); + case CHANGED_FILES_COVERAGE_TABLE_ID: + return new ChangedFilesCoverageTable(tableId, getNode(), changeCoverageTreeRoot, renderer, + colorProvider); case INDIRECT_COVERAGE_TABLE_ID: return new IndirectCoverageChangesTable(tableId, getNode(), indirectCoverageChangesTreeRoot, renderer, colorProvider); @@ -489,14 +495,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/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/io/jenkins/plugins/coverage/metrics/steps/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties index 83dd1df40..2ae9ab514 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,10 @@ 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 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..1e77346b6 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java @@ -0,0 +1,125 @@ +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.api.Test; +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.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"; + + @Test + void shouldCreate() { + var action = createCoverageBuildAction(); + var publisher = new CoverageChecksPublisher(action, REPORT_NAME, 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"); + assertOutput(checkDetails); + } + + private void assertOutput(final ChecksDetails checkDetails) { + assertThat(checkDetails.getOutput()).isPresent().get().satisfies(output -> { + assertThat(output.getTitle()).isPresent() + .get() + .isEqualTo("Change Coverage: Line: 50.00% (Lines of Code: 323)"); + assertThat(output.getText()).isEmpty(); + assertSummary(output); + assertChecksAnnotations(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) { + assertThat(checksOutput.getChecksAnnotations()).hasSize(2); + assertThat(checksOutput.getChecksAnnotations().get(0)).satisfies(annotation -> { + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).isPresent().get() + .isEqualTo("Changed line #L160 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(160); + assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(160); + }); + assertThat(checksOutput.getChecksAnnotations().get(1)).satisfies(annotation -> { + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).isPresent().get() + .isEqualTo("Changed lines #L162 - L164 are not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(162); + assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(164); + }); + } + + 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.getAllFileNodes().stream().filter(file -> file.getName().equals("TreeStringBuilder.java")).findFirst() + .ifPresent(file -> { + assertThat(file.getCoveredLines()).contains(160, 162, 163, 164); + file.addChangedLine(160); + file.addChangedLine(162); + file.addChangedLine(163); + file.addChangedLine(164); + }); + + 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..bb4c6d220 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 @@ -110,6 +110,14 @@ void shouldProvideSelectedColumn() { assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_LINES_DELTA); assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#changeCoverage"); + column.setBaseline(Baseline.MODIFIED_FILES); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_FILES); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#changedFilesCoverage"); + + column.setBaseline(Baseline.MODIFIED_FILES_DELTA); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_FILES_DELTA); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#changedFilesCoverage"); + column.setBaseline(Baseline.INDIRECT); assertThat(column.getBaseline()).isEqualTo(Baseline.INDIRECT); assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#indirectCoverage"); @@ -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/CoveragePercentageTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java index 215736efa..ab71e661d 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java @@ -78,7 +78,7 @@ void shouldFormatPercentage() { @Test void shouldFormatDeltaPercentage() { CoveragePercentage coveragePercentage = CoveragePercentage.valueOf(COVERAGE_PERCENTAGE); - assertThat(coveragePercentage.formatDeltaPercentage(LOCALE)).isEqualTo("+50,00%"); + assertThat(coveragePercentage.formatDeltaPercentage(LOCALE)).isEqualTo("+50,00"); } @Test 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 d7cc78a7c..8e6ac77c1 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 @@ -159,7 +159,7 @@ void shouldConvertMetricMap2String() { assertThat(converter.marshal(map)).isEqualTo("[BRANCH: 50/100]"); map.put(LINE, Fraction.getFraction(3, 4)); - assertThat(converter.marshal(map)).isEqualTo("[LINE: 3/4, BRANCH: 50/100]"); + assertThat(converter.marshal(map)).isEqualTo("[BRANCH: 50/100, LINE: 3/4]"); } @Test @@ -170,7 +170,7 @@ void shouldConvertString2MetricMap() { Fraction first = Fraction.getFraction(50, 100); Assertions.assertThat(converter.unmarshal("[BRANCH: 50/100]")) .containsExactly(entry(BRANCH, first)); - Assertions.assertThat(converter.unmarshal("[LINE: 3/4, BRANCH: 50/100]")) + Assertions.assertThat(converter.unmarshal("[LINE: 3/4]")) .containsExactly(entry(LINE, Fraction.getFraction(3, 4)), entry(BRANCH, first)); } @@ -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/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..af2974155 --- /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: 91.02% (+50.00) + * Branch: 93.97% (n/a) + * Cyclomatic Complexity Density: 0.50% + * Lines of Code: 323 +* **[Changed files (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#changedFilesCoverage)** + * Line: 50.00% (+50.00) + * Branch: n/a (n/a) + * Cyclomatic Complexity Density: n/a + * Lines of Code: 53 +* **[Changed code lines (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#changeCoverage)** + * Line: 50.00% (+50.00) + * Branch: n/a (n/a) + * Lines of Code: 4 +* **[Indirect changes](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#indirectCoverage)** + * Line: 50.00% + * Branch: n/a + * Lines of Code: n/a + + +## Quality Gates Summary - PASSED + + + +## Project Coverage Summary + +|Container|Module|Package|File|Class|Method|Branch|Line|Instruction| +|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +|: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)|93.97% (109/116)|91.02% (294/323)|93.33% (1260/1350)| +|:chart_with_upwards_trend: **Overall project (difference to reference job)**|-|+20.00 :arrow_up:|-|-|-|-|-|+50.00 :arrow_up:|-| From 5686bea0771d35c24fc2b5a08882866a5151c13d Mon Sep 17 00:00:00 2001 From: Florian Orendi Date: Sun, 5 Feb 2023 18:37:11 +0100 Subject: [PATCH 02/23] Add Modified Files Coverage and GitHub Checks API --- .../coverage/metrics/model/Baseline.java | 4 +- .../metrics/model/ElementFormatter.java | 51 +++ .../steps/ChangedFilesCoverageTable.java | 109 +++++ .../metrics/steps/CoverageBuildAction.java | 57 ++- .../steps/CoverageChecksPublisher.java | 416 ++++++++++++++++++ .../metrics/steps/CoveragePercentage.java | 2 +- .../metrics/steps/CoverageRecorder.java | 11 + .../metrics/steps/CoverageReporter.java | 10 + .../metrics/steps/CoverageTableModel.java | 6 +- .../metrics/steps/CoverageViewModel.java | 18 +- .../coverage/model/CoverageReporter.java | 2 +- .../metrics/steps/Messages.properties | 7 + .../steps/CoverageBuildActionTest.java | 6 +- .../steps/CoverageChecksPublisherTest.java | 125 ++++++ .../steps/CoverageMetricColumnTest.java | 10 +- .../metrics/steps/CoveragePercentageTest.java | 2 +- .../metrics/steps/CoverageXmlStreamTest.java | 6 +- .../steps/coverage-publisher-summary.MD | 32 ++ 18 files changed, 846 insertions(+), 28 deletions(-) create mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java create mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java create mode 100644 plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java create mode 100644 plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.MD 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..0eff1eee3 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 @@ -38,12 +38,12 @@ public enum Baseline { * 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(), "changedFilesCoverage", 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(), "changedFilesCoverage", 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..fc73491b4 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,13 @@ package io.jenkins.plugins.coverage.metrics.model; +import java.util.Comparator; +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 +16,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; @@ -370,6 +376,51 @@ 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() + // use the default comparator which uses the enum ordinal + .sorted() + .map(this::getDisplayName) + .collect(Collectors.toList()); + } + + /** + * Formats a stream of values to theis display representation by using the passed 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) + .sorted(Comparator.comparing(Coverage::getMetric)); + } + /** * Returns a localized human-readable label for the specified metric. * diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java new file mode 100644 index 000000000..ee6b707af --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java @@ -0,0 +1,109 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +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 hudson.Functions; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; +import io.jenkins.plugins.datatables.DetailedCell; +import io.jenkins.plugins.datatables.TableConfiguration; +import io.jenkins.plugins.datatables.TableConfiguration.SelectStyle; + +/** + * {@link CoverageTableModel} implementation for visualizing the changed files coverage. + * + * @since 4.0.0 + */ +class ChangedFilesCoverageTable extends CoverageTableModel { + private final Node changedFilesCoverageRoot; + + /** + * Creates a changed files coverage table model. + * + * @param id + * The ID of the table + * @param root + * The root of the origin coverage tree + * @param changeRoot + * The root of the change coverage tree + * @param renderer + * the renderer to use for the file names + * @param colorProvider + * The {@link ColorProvider} which provides the used colors + */ + ChangedFilesCoverageTable(final String id, final Node root, final Node changeRoot, + final RowRenderer renderer, final ColorProvider colorProvider) { + super(id, root, renderer, colorProvider); + + this.changedFilesCoverageRoot = changeRoot; + } + + @Override + public TableConfiguration getTableConfiguration() { + return super.getTableConfiguration().select(SelectStyle.SINGLE); + } + + @Override + public List getRows() { + Locale browserLocale = Functions.getCurrentLocale(); + return changedFilesCoverageRoot.getAllFileNodes().stream() + .map(file -> new ChangedFilesCoverageRow( + getOriginalNode(file), file, browserLocale, getRenderer(), getColorProvider())) + .collect(Collectors.toList()); + } + + private FileNode getOriginalNode(final FileNode fileNode) { + return getRoot().getAllFileNodes().stream() + .filter(node -> node.getPath().equals(fileNode.getPath()) + && node.getName().equals(fileNode.getName())) + .findFirst() + .orElse(fileNode); // return this as fallback to prevent exceptions + } + + /** + * UI row model for the changed files coverage details table. + * + * @since 4.0.0 + */ + private static class ChangedFilesCoverageRow extends CoverageRow { + private final FileNode originalFile; + + ChangedFilesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, + final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { + super(changedFileNode, browserLocale, renderer, colorProvider); + + this.originalFile = originalFile; + } + + @Override + public DetailedCell getLineCoverageDelta() { + return createColoredChangeCoverageDeltaColumn(Metric.LINE); + } + + @Override + public DetailedCell getBranchCoverageDelta() { + return createColoredChangeCoverageDeltaColumn(Metric.BRANCH); + } + + @Override + public int getLoc() { + return getFile().getCoveredLines().size(); + } + + private DetailedCell createColoredChangeCoverageDeltaColumn(final Metric metric) { + Coverage fileCoverage = getFile().getTypedValue(metric, Coverage.nullObject(metric)); + if (fileCoverage.isSet()) { + return createColoredCoverageDeltaColumn(metric, + fileCoverage.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..abea0119b 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 @@ -80,6 +80,12 @@ public final class CoverageBuildAction extends BuildAction implements Stap /** The delta of the coverages of the associated change request with respect to the reference build. */ private final NavigableMap changeCoverageDifference; + /** The coverage of the changed files. */ + private final List changedFilesCoverage; + + /** The coverage delta of the changed files. */ + private final NavigableMap changedFilesCoverageDifference; + /** The indirect coverage changes of the associated change request with respect to the reference build. */ private final List indirectCoverageChanges; @@ -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()); } /** @@ -150,9 +156,12 @@ public CoverageBuildAction(final Run owner, final String id, final String final NavigableMap delta, final List changeCoverage, final NavigableMap changeCoverageDifference, + final List changedFilesCoverage, + final NavigableMap changedFilesCoverageDifference, final List indirectCoverageChanges) { this(owner, id, optionalName, icon, result, qualityGateResult, log, referenceBuildId, delta, changeCoverage, - changeCoverageDifference, indirectCoverageChanges, true); + changeCoverageDifference, changedFilesCoverage, changedFilesCoverageDifference, indirectCoverageChanges, + true); } @VisibleForTesting @@ -163,6 +172,8 @@ public CoverageBuildAction(final Run owner, final String id, final String final NavigableMap delta, final List changeCoverage, final NavigableMap changeCoverageDifference, + final List changedFilesCoverage, + final NavigableMap changedFilesCoverageDifference, final List indirectCoverageChanges, final boolean canSerialize) { super(owner, result, false); @@ -177,6 +188,8 @@ public CoverageBuildAction(final Run owner, final String id, final String difference = delta; this.changeCoverage = new ArrayList<>(changeCoverage); this.changeCoverageDifference = changeCoverageDifference; + this.changedFilesCoverage = new ArrayList<>(changedFilesCoverage); + this.changedFilesCoverageDifference = changedFilesCoverageDifference; this.indirectCoverageChanges = new ArrayList<>(indirectCoverageChanges); this.referenceBuildId = referenceBuildId; @@ -208,7 +221,7 @@ public ElementFormatter getFormatter() { public CoverageStatistics getStatistics() { return new CoverageStatistics(projectValues, difference, changeCoverage, changeCoverageDifference, - List.of(), new TreeMap<>()); + changedFilesCoverage, changedFilesCoverageDifference); } /** @@ -218,7 +231,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 +286,18 @@ 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 changeCoverageDifference; + } + else if (deltaBaseline == Baseline.MODIFIED_FILES_DELTA) { + return changedFilesCoverageDifference; + } + throw new NoSuchElementException("No delta baseline: " + deltaBaseline); + } + /** * Returns all important values for the specified baseline. * @@ -300,12 +325,23 @@ private Stream getValueStream(final Baseline baseline) { if (baseline == Baseline.MODIFIED_LINES) { return changeCoverage.stream(); } + if (baseline == Baseline.MODIFIED_FILES) { + return changedFilesCoverage.stream(); + } if (baseline == Baseline.INDIRECT) { return indirectCoverageChanges.stream(); } throw new NoSuchElementException("No such baseline: " + baseline); } + public String formatValueForMetric(final Baseline baseline, final Metric metric) { + var valueOfMetric = getAllValues(baseline).stream() + .filter(value -> value.getMetric().equals(metric)) + .findFirst(); + return valueOfMetric.map(this::formatValueWithMetric) + .orElseGet(() -> FORMATTER.getDisplayName(metric) + ": " + io.jenkins.plugins.coverage.metrics.steps.Messages.Coverage_Not_Available()); + } + /** * Returns whether a delta metric for the specified baseline exists. * @@ -316,7 +352,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; } /** @@ -337,6 +374,10 @@ public boolean hasDelta(final Baseline baseline, final Metric metric) { return changeCoverageDifference.containsKey(metric) && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); } + if (baseline == Baseline.MODIFIED_FILES) { + return changedFilesCoverageDifference.containsKey(metric) + && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); + } if (baseline == Baseline.INDIRECT) { return false; } @@ -368,6 +409,12 @@ public String formatDelta(final Baseline baseline, final Metric metric) { Functions.getCurrentLocale()); } } + if (baseline == Baseline.MODIFIED_FILES) { + if (hasDelta(baseline, metric)) { + return FORMATTER.formatDelta(changedFilesCoverageDifference.get(metric), metric, + Functions.getCurrentLocale()); + } + } return Messages.Coverage_Not_Available(); } 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..8c6963199 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java @@ -0,0 +1,416 @@ +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.function.BiFunction; + +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.util.JenkinsFacade; +import io.jenkins.plugins.util.QualityGateStatus; + +/** + * Publishes coverage as Checks to SCM platforms. + * + * @author Florian Orendi + */ +class CoverageChecksPublisher { + + private final ElementFormatter formatter; + + private final CoverageBuildAction action; + private final JenkinsFacade jenkinsFacade; + private final String checksName; + + CoverageChecksPublisher(final CoverageBuildAction action, final String checksName) { + this(action, checksName, new JenkinsFacade()); + } + + @VisibleForTesting + CoverageChecksPublisher(final CoverageBuildAction action, + final String checksName, final JenkinsFacade jenkinsFacade) { + this.jenkinsFacade = jenkinsFacade; + this.action = action; + this.checksName = checksName; + this.formatter = new ElementFormatter(); + } + + void publishChecks(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("Change Coverage: %s (%s)", + action.formatValueForMetric(Baseline.MODIFIED_LINES, Metric.LINE), + formatValueOfMetric(action.getResult(), Metric.LOC)); + } + + private String getSummary() { + var root = action.getResult(); + return getOverallCoverageSummary(root) + "\n\n" + + getHealthReportSummary() + "\n\n" + + getProjectMetricsSummary(root); + } + + private List getAnnotations() { + var annotations = new ArrayList(); + for (var fileNode : action.getResult().filterChanges().getAllFileNodes()) { + for (var aggregatedLines : getAggregatedMissingLines(fileNode)) { + ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder() + .withPath(fileNode.getPath()) + .withTitle(Messages.Checks_Annotation_Title()) + .withAnnotationLevel(ChecksAnnotationLevel.WARNING) + .withMessage(getAnnotationMessage(aggregatedLines)) + .withStartLine(aggregatedLines.startLine) + .withEndLine(aggregatedLines.endLine); + + annotations.add(builder.build()); + } + } + + return annotations; + } + + private String getAnnotationMessage(final AggregatedMissingLines aggregatedMissingLines) { + if (aggregatedMissingLines.startLine == aggregatedMissingLines.endLine) { + return Messages.Checks_Annotation_Message_SingleLine(aggregatedMissingLines.startLine); + } + return Messages.Checks_Annotation_Message_MultiLine(aggregatedMissingLines.startLine, + aggregatedMissingLines.endLine); + } + + private List getAggregatedMissingLines(final FileNode fileNode) { + var aggregatedMissingLines = new ArrayList(); + + if (fileNode.hasCoveredLinesInChangeSet()) { + var linesWithCoverage = fileNode.getCoveredLines(); + // there has to be at least one line when it is a file node with changes + var previousLine = linesWithCoverage.first(); + var aggregatedLines = new AggregatedMissingLines(previousLine); + linesWithCoverage.remove(previousLine); + + for (final var line : linesWithCoverage) { + if (line == previousLine + 1) { + aggregatedLines.increaseEndLine(); + } + else { + aggregatedMissingLines.add(aggregatedLines); + aggregatedLines = new AggregatedMissingLines(line); + } + previousLine = line; + } + + aggregatedMissingLines.add(aggregatedLines); + } + + return aggregatedMissingLines; + } + + 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 changedFilesCoverageRoot = root.filterByChangedFilesCoverage(); + var changeCoverageRoot = root.filterChanges(); + var indirectlyChangedCoverage = root.filterByIndirectlyChangedCoverage(); + + var projectCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.PROJECT_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.PROJECT_DELTA.getUrl()))); + var changedFilesCoverageHeader = getBulletListItem(1, + formatText(TextFormat.BOLD, getUrlText(Baseline.MODIFIED_FILES_DELTA.getTitle(), + getCoverageReportBaseUrl() + Baseline.MODIFIED_FILES_DELTA.getUrl()))); + var changeCoverageHeader = 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, action::formatValueForMetric, Baseline.PROJECT)); + var projectCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.PROJECT)); + var projectCoverageComplexity = getBulletListItem(2, formatValueOfMetric(root, Metric.COMPLEXITY_DENSITY)); + var projectCoverageLoc = getBulletListItem(2, formatValueOfMetric(root, Metric.LOC)); + + var changedFilesCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, action::formatValueForMetric, Baseline.MODIFIED_FILES)); + var changedFilesCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.MODIFIED_FILES)); + var changedFilesCoverageComplexity = getBulletListItem(2, + formatValueOfMetric(changedFilesCoverageRoot, Metric.COMPLEXITY_DENSITY)); + var changedFilesCoverageLoc = getBulletListItem(2, + formatValueOfMetric(changedFilesCoverageRoot, Metric.LOC)); + + var changeCoverageLine = getBulletListItem(2, + formatCoverageForMetric(Metric.LINE, action::formatValueForMetric, Baseline.MODIFIED_LINES)); + var changeCoverageBranch = getBulletListItem(2, + formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.MODIFIED_LINES)); + var changeCoverageLoc = getBulletListItem(2, formatValueOfMetric(changeCoverageRoot, Metric.LOC)); + + var indirectCoverageChangesLine = getBulletListItem(2, + action.formatValueForMetric(Baseline.INDIRECT, Metric.LINE)); + var indirectCoverageChangesBranch = getBulletListItem(2, + action.formatValueForMetric(Baseline.INDIRECT, Metric.BRANCH)); + var indirectCoverageChangesLoc = getBulletListItem(2, + formatValueOfMetric(indirectlyChangedCoverage, Metric.LOC)); + + return sectionHeader + + projectCoverageHeader + + projectCoverageLine + + projectCoverageBranch + + projectCoverageComplexity + + projectCoverageLoc + + changedFilesCoverageHeader + + changedFilesCoverageLine + + changedFilesCoverageBranch + + changedFilesCoverageComplexity + + changedFilesCoverageLoc + + changeCoverageHeader + + changeCoverageLine + + changeCoverageBranch + + changeCoverageLoc + + 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 getHealthReportSummary() { + return getSectionHeader(2, Messages.Checks_QualityGates(action.getQualityGateResult().toString())); + } + + 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 BiFunction coverageFormat, + final Baseline baseline) { + return String.format("%s (%s)", + coverageFormat.apply(baseline, metric), action.formatDelta(baseline, metric)); + } + + private String formatValueOfMetric(final Node root, final Metric metric) { + var value = root.getValue(metric); + return value.map(action::formatValueWithDetails) + .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 + } + + private static class AggregatedMissingLines { + private final int startLine; + private int endLine; + + private AggregatedMissingLines(final int startLine) { + this.startLine = startLine; + this.endLine = startLine; + } + + private void increaseEndLine() { + endLine++; + } + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java index f0dad34fd..4fadc518d 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java @@ -147,7 +147,7 @@ public String formatPercentage(final Locale locale) { * @return the formatted delta percentage as plain text with a leading sign */ public String formatDeltaPercentage(final Locale locale) { - return String.format(locale, "%+.2f%%", getDoubleValue()); + return String.format(locale, "%+.2f", getDoubleValue()); } public int getNumerator() { 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..8a32f223a 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,9 @@ */ @SuppressWarnings("checkstyle:ClassFanOutComplexity") public class CoverageRecorder extends Recorder { + // TODO: make customizable? + private 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. */ @@ -353,6 +356,14 @@ void perform(final Run run, final FilePath workspace, final TaskListener t getSourceCodeEncoding(), getSourceCodeRetention(), resultHandler); } } + + if (!skipPublishingChecks) { + var coverageAction = run.getAction(CoverageBuildAction.class); + if (coverageAction != null) { + var checksPublisher = new CoverageChecksPublisher(coverageAction, CHECKS_DEFAULT_NAME); + checksPublisher.publishChecks(taskListener); + } + } } else { logHandler.log("Skipping execution of coverage recorder since overall result is '%s'", overallResult); 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..7f0e47255 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; @@ -62,11 +63,18 @@ void publishAction(final String id, final String optionalName, final String icon Node changeCoverageRoot = rootNode.filterChanges(); NavigableMap changeCoverageDelta; + List aggregatedChangedFilesCoverage; + NavigableMap changedFilesCoverageDelta; if (hasChangeCoverage(changeCoverageRoot)) { changeCoverageDelta = changeCoverageRoot.computeDelta(rootNode); + Node changedFilesCoverageRoot = rootNode.filterByChangedFilesCoverage(); + aggregatedChangedFilesCoverage = changedFilesCoverageRoot.aggregateValues(); + changedFilesCoverageDelta = changedFilesCoverageRoot.computeDelta(rootNode); } else { changeCoverageDelta = new TreeMap<>(); + aggregatedChangedFilesCoverage = new ArrayList<>(); + changedFilesCoverageDelta = new TreeMap<>(); if (rootNode.hasChangedLines()) { log.logInfo("No detected code changes affect the code coverage"); } @@ -85,6 +93,8 @@ void publishAction(final String id, final String optionalName, final String icon coverageDelta, changeCoverageRoot.aggregateValues(), changeCoverageDelta, + aggregatedChangedFilesCoverage, + changedFilesCoverageDelta, indirectCoverageChangesTree.aggregateValues()); if (sourceCodeRetention == SourceCodeRetention.MODIFIED) { filesToStore = changeCoverageRoot.getAllFileNodes(); 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..7851af459 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 @@ -115,7 +115,7 @@ public List getColumns() { .withType(ColumnType.NUMBER) .build(); columns.add(loc); - if (root.containsMetric(Metric.COMPLEXITY)) { + if (root.getMetrics().contains(Metric.COMPLEXITY)) { TableColumn complexity = new ColumnBuilder().withHeaderLabel(Messages.Column_Complexity()) .withDataPropertyKey("complexity") .withResponsivePriority(500) @@ -123,7 +123,7 @@ public List getColumns() { .build(); columns.add(complexity); } - if (root.containsMetric(Metric.COMPLEXITY_DENSITY)) { + if (root.getMetrics().contains(Metric.COMPLEXITY_DENSITY)) { TableColumn complexity = new ColumnBuilder().withHeaderLabel(Messages.Column_ComplexityDensity()) .withDataPropertyKey("density") .withDetailedCell() @@ -137,7 +137,7 @@ public List getColumns() { private void configureValueColumn(final String key, final Metric metric, final String headerLabel, final String deltaHeaderLabel, final List columns) { - if (root.containsMetric(metric)) { + if (root.getMetrics().contains(metric)) { TableColumn lineCoverage = new ColumnBuilder().withHeaderLabel(headerLabel) .withDataPropertyKey(key) .withDetailedCell() 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..5788b6cb0 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; @@ -69,6 +68,8 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen static final String ABSOLUTE_COVERAGE_TABLE_ID = "absolute-coverage-table"; static final String CHANGE_COVERAGE_TABLE_ID = "change-coverage-table"; + + static final String CHANGED_FILES_COVERAGE_TABLE_ID = "changed-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"; @@ -85,6 +86,7 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen private final String id; private final Node changeCoverageTreeRoot; + private final Node changedFilesCoverageTreeRoot; private final Node indirectCoverageChangesTreeRoot; private final Function trendChartFunction; @@ -110,6 +112,7 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen // initialize filtered coverage trees so that they will not be calculated multiple times changeCoverageTreeRoot = node.filterChanges(); + changedFilesCoverageTreeRoot = node.filterByChangedFilesCoverage(); indirectCoverageChangesTreeRoot = node.filterByIndirectlyChangedCoverage(); this.trendChartFunction = trendChartFunction; } @@ -294,6 +297,9 @@ public TableModel getTableModel(final String tableId) { case CHANGE_COVERAGE_TABLE_ID: return new ChangeCoverageTableModel(tableId, getNode(), changeCoverageTreeRoot, renderer, colorProvider); + case CHANGED_FILES_COVERAGE_TABLE_ID: + return new ChangedFilesCoverageTable(tableId, getNode(), changeCoverageTreeRoot, renderer, + colorProvider); case INDIRECT_COVERAGE_TABLE_ID: return new IndirectCoverageChangesTable(tableId, getNode(), indirectCoverageChangesTreeRoot, renderer, colorProvider); @@ -489,14 +495,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/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/io/jenkins/plugins/coverage/metrics/steps/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties index 83dd1df40..2ae9ab514 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,10 @@ 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 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..1e77346b6 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java @@ -0,0 +1,125 @@ +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.api.Test; +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.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"; + + @Test + void shouldCreate() { + var action = createCoverageBuildAction(); + var publisher = new CoverageChecksPublisher(action, REPORT_NAME, 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"); + assertOutput(checkDetails); + } + + private void assertOutput(final ChecksDetails checkDetails) { + assertThat(checkDetails.getOutput()).isPresent().get().satisfies(output -> { + assertThat(output.getTitle()).isPresent() + .get() + .isEqualTo("Change Coverage: Line: 50.00% (Lines of Code: 323)"); + assertThat(output.getText()).isEmpty(); + assertSummary(output); + assertChecksAnnotations(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) { + assertThat(checksOutput.getChecksAnnotations()).hasSize(2); + assertThat(checksOutput.getChecksAnnotations().get(0)).satisfies(annotation -> { + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).isPresent().get() + .isEqualTo("Changed line #L160 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(160); + assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(160); + }); + assertThat(checksOutput.getChecksAnnotations().get(1)).satisfies(annotation -> { + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).isPresent().get() + .isEqualTo("Changed lines #L162 - L164 are not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(162); + assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(164); + }); + } + + 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.getAllFileNodes().stream().filter(file -> file.getName().equals("TreeStringBuilder.java")).findFirst() + .ifPresent(file -> { + assertThat(file.getCoveredLines()).contains(160, 162, 163, 164); + file.addChangedLine(160); + file.addChangedLine(162); + file.addChangedLine(163); + file.addChangedLine(164); + }); + + 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..bb4c6d220 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 @@ -110,6 +110,14 @@ void shouldProvideSelectedColumn() { assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_LINES_DELTA); assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#changeCoverage"); + column.setBaseline(Baseline.MODIFIED_FILES); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_FILES); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#changedFilesCoverage"); + + column.setBaseline(Baseline.MODIFIED_FILES_DELTA); + assertThat(column.getBaseline()).isEqualTo(Baseline.MODIFIED_FILES_DELTA); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#changedFilesCoverage"); + column.setBaseline(Baseline.INDIRECT); assertThat(column.getBaseline()).isEqualTo(Baseline.INDIRECT); assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#indirectCoverage"); @@ -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/CoveragePercentageTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java index 215736efa..ab71e661d 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java @@ -78,7 +78,7 @@ void shouldFormatPercentage() { @Test void shouldFormatDeltaPercentage() { CoveragePercentage coveragePercentage = CoveragePercentage.valueOf(COVERAGE_PERCENTAGE); - assertThat(coveragePercentage.formatDeltaPercentage(LOCALE)).isEqualTo("+50,00%"); + assertThat(coveragePercentage.formatDeltaPercentage(LOCALE)).isEqualTo("+50,00"); } @Test 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 d7cc78a7c..8e6ac77c1 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 @@ -159,7 +159,7 @@ void shouldConvertMetricMap2String() { assertThat(converter.marshal(map)).isEqualTo("[BRANCH: 50/100]"); map.put(LINE, Fraction.getFraction(3, 4)); - assertThat(converter.marshal(map)).isEqualTo("[LINE: 3/4, BRANCH: 50/100]"); + assertThat(converter.marshal(map)).isEqualTo("[BRANCH: 50/100, LINE: 3/4]"); } @Test @@ -170,7 +170,7 @@ void shouldConvertString2MetricMap() { Fraction first = Fraction.getFraction(50, 100); Assertions.assertThat(converter.unmarshal("[BRANCH: 50/100]")) .containsExactly(entry(BRANCH, first)); - Assertions.assertThat(converter.unmarshal("[LINE: 3/4, BRANCH: 50/100]")) + Assertions.assertThat(converter.unmarshal("[LINE: 3/4]")) .containsExactly(entry(LINE, Fraction.getFraction(3, 4)), entry(BRANCH, first)); } @@ -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/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..af2974155 --- /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: 91.02% (+50.00) + * Branch: 93.97% (n/a) + * Cyclomatic Complexity Density: 0.50% + * Lines of Code: 323 +* **[Changed files (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#changedFilesCoverage)** + * Line: 50.00% (+50.00) + * Branch: n/a (n/a) + * Cyclomatic Complexity Density: n/a + * Lines of Code: 53 +* **[Changed code lines (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#changeCoverage)** + * Line: 50.00% (+50.00) + * Branch: n/a (n/a) + * Lines of Code: 4 +* **[Indirect changes](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#indirectCoverage)** + * Line: 50.00% + * Branch: n/a + * Lines of Code: n/a + + +## Quality Gates Summary - PASSED + + + +## Project Coverage Summary + +|Container|Module|Package|File|Class|Method|Branch|Line|Instruction| +|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +|: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)|93.97% (109/116)|91.02% (294/323)|93.33% (1260/1350)| +|:chart_with_upwards_trend: **Overall project (difference to reference job)**|-|+20.00 :arrow_up:|-|-|-|-|-|+50.00 :arrow_up:|-| From bf9b3a54e831441873ad0098e084f4bab6f40e49 Mon Sep 17 00:00:00 2001 From: Florian Orendi Date: Sat, 11 Feb 2023 13:02:07 +0100 Subject: [PATCH 03/23] Rename Change Coverage to Modified Lines Coverage an fix tests --- .../coverage/metrics/model/Baseline.java | 8 +- .../metrics/source/SourceCodeFacade.java | 4 +- .../metrics/steps/ChangesTableModel.java | 12 +- .../metrics/steps/CoverageBuildAction.java | 118 +++++++++++------- .../steps/CoverageChecksPublisher.java | 107 ++++++++-------- .../metrics/steps/CoveragePercentage.java | 2 +- .../metrics/steps/CoverageReporter.java | 50 ++++---- .../metrics/steps/CoverageViewModel.java | 31 +++-- ...e.java => ModifiedFilesCoverageTable.java} | 32 ++--- ...a => ModifiedLinesCoverageTableModel.java} | 12 +- .../metrics/source/SourceCodeFacadeTest.java | 15 +-- .../steps/CoverageChecksPublisherTest.java | 12 +- .../steps/CoverageMetricColumnTest.java | 8 +- .../metrics/steps/CoveragePercentageTest.java | 2 +- .../metrics/steps/CoverageViewModelTest.java | 4 +- .../metrics/steps/CoverageXmlStreamTest.java | 4 +- .../metrics/steps/DeltaComputationITest.java | 6 +- .../metrics/steps/GitForensicsITest.java | 14 +-- 18 files changed, 236 insertions(+), 205 deletions(-) rename plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/{ChangedFilesCoverageTable.java => ModifiedFilesCoverageTable.java} (72%) rename plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/{ChangeCoverageTableModel.java => ModifiedLinesCoverageTableModel.java} (67%) 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 0eff1eee3..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(), "changedFilesCoverage", 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(), "changedFilesCoverage", 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/source/SourceCodeFacade.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacade.java index 4afd6bb58..c31518b7e 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,7 +172,7 @@ 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()); Set linesAsText = lines.stream().map(String::valueOf).collect(Collectors.toSet()); 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 abea0119b..59141c13a 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,16 +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 changed files. */ - private final List changedFilesCoverage; + /** The coverage of the modified lines. */ + private final List modifiedFilesCoverage; - /** The coverage delta of the changed files. */ - private final NavigableMap changedFilesCoverageDifference; + /** 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; @@ -93,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()); } @@ -142,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 @@ -154,13 +154,15 @@ 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 changedFilesCoverage, - final NavigableMap changedFilesCoverageDifference, + 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, changedFilesCoverage, changedFilesCoverageDifference, indirectCoverageChanges, + this(owner, id, optionalName, icon, result, qualityGateResult, log, referenceBuildId, delta, + modifiedLinesCoverage, + modifiedLinesCoverageDifference, modifiedFilesCoverage, modifiedFilesCoverageDifference, + indirectCoverageChanges, true); } @@ -170,10 +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 changedFilesCoverage, - final NavigableMap changedFilesCoverageDifference, + final List modifiedLinesCoverage, + final NavigableMap modifiedLinesCoverageDifference, + final List modifiedFilesCoverage, + final NavigableMap modifiedFilesCoverageDifference, final List indirectCoverageChanges, final boolean canSerialize) { super(owner, result, false); @@ -186,10 +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.changedFilesCoverage = new ArrayList<>(changedFilesCoverage); - this.changedFilesCoverageDifference = changedFilesCoverageDifference; + this.modifiedLinesCoverage = new ArrayList<>(modifiedLinesCoverage); + this.modifiedLinesCoverageDifference = modifiedLinesCoverageDifference; + this.modifiedFilesCoverage = new ArrayList<>(modifiedFilesCoverage); + this.modifiedFilesCoverageDifference = modifiedFilesCoverageDifference; this.indirectCoverageChanges = new ArrayList<>(indirectCoverageChanges); this.referenceBuildId = referenceBuildId; @@ -220,8 +222,8 @@ public ElementFormatter getFormatter() { } public CoverageStatistics getStatistics() { - return new CoverageStatistics(projectValues, difference, changeCoverage, changeCoverageDifference, - changedFilesCoverage, changedFilesCoverageDifference); + return new CoverageStatistics(projectValues, difference, modifiedLinesCoverage, modifiedLinesCoverageDifference, + modifiedFilesCoverage, modifiedFilesCoverageDifference); } /** @@ -289,11 +291,12 @@ public List getAllValues(final Baseline baseline) { public NavigableMap getAllDeltas(final Baseline deltaBaseline) { if (deltaBaseline == Baseline.PROJECT_DELTA) { return difference; - } else if (deltaBaseline == Baseline.MODIFIED_LINES_DELTA) { - return changeCoverageDifference; + } + else if (deltaBaseline == Baseline.MODIFIED_LINES_DELTA) { + return modifiedLinesCoverageDifference; } else if (deltaBaseline == Baseline.MODIFIED_FILES_DELTA) { - return changedFilesCoverageDifference; + return modifiedFilesCoverageDifference; } throw new NoSuchElementException("No delta baseline: " + deltaBaseline); } @@ -313,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()); @@ -323,10 +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 changedFilesCoverage.stream(); + return modifiedFilesCoverage.stream(); } if (baseline == Baseline.INDIRECT) { return indirectCoverageChanges.stream(); @@ -334,14 +343,6 @@ private Stream getValueStream(final Baseline baseline) { throw new NoSuchElementException("No such baseline: " + baseline); } - public String formatValueForMetric(final Baseline baseline, final Metric metric) { - var valueOfMetric = getAllValues(baseline).stream() - .filter(value -> value.getMetric().equals(metric)) - .findFirst(); - return valueOfMetric.map(this::formatValueWithMetric) - .orElseGet(() -> FORMATTER.getDisplayName(metric) + ": " + io.jenkins.plugins.coverage.metrics.steps.Messages.Coverage_Not_Available()); - } - /** * Returns whether a delta metric for the specified baseline exists. * @@ -371,11 +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 changedFilesCoverageDifference.containsKey(metric) + return modifiedFilesCoverageDifference.containsKey(metric) && Set.of(Metric.BRANCH, Metric.LINE).contains(metric); } if (baseline == Baseline.INDIRECT) { @@ -384,6 +385,39 @@ 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 = getAllValues(baseline).stream() + .filter(v -> v.getMetric() == metric) + .findFirst(); + 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). @@ -405,13 +439,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(changedFilesCoverageDifference.get(metric), 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 index 8c6963199..f9b9dcfd2 100644 --- 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 @@ -6,7 +6,6 @@ import java.util.List; import java.util.NavigableMap; import java.util.TreeMap; -import java.util.function.BiFunction; import org.apache.commons.lang3.math.Fraction; @@ -40,7 +39,7 @@ */ class CoverageChecksPublisher { - private final ElementFormatter formatter; + private final ElementFormatter FORMATTER = new ElementFormatter(); private final CoverageBuildAction action; private final JenkinsFacade jenkinsFacade; @@ -56,7 +55,6 @@ class CoverageChecksPublisher { this.jenkinsFacade = jenkinsFacade; this.action = action; this.checksName = checksName; - this.formatter = new ElementFormatter(); } void publishChecks(final TaskListener listener) { @@ -82,9 +80,9 @@ ChecksDetails extractChecksDetails() { } private String getChecksTitle() { - return String.format("Change Coverage: %s (%s)", - action.formatValueForMetric(Baseline.MODIFIED_LINES, Metric.LINE), - formatValueOfMetric(action.getResult(), Metric.LOC)); + return String.format("%s: %s", + FORMATTER.getDisplayName(Baseline.MODIFIED_LINES), + action.formatValue(Baseline.MODIFIED_LINES, Metric.LINE)); } private String getSummary() { @@ -125,7 +123,7 @@ private List getAggregatedMissingLines(final FileNode fi var aggregatedMissingLines = new ArrayList(); if (fileNode.hasCoveredLinesInChangeSet()) { - var linesWithCoverage = fileNode.getCoveredLines(); + var linesWithCoverage = fileNode.getLinesWithCoverage(); // there has to be at least one line when it is a file node with changes var previousLine = linesWithCoverage.first(); var aggregatedLines = new AggregatedMissingLines(previousLine); @@ -155,17 +153,17 @@ private String getCoverageReportBaseUrl() { private String getOverallCoverageSummary(final Node root) { String sectionHeader = getSectionHeader(2, Messages.Checks_Summary()); - var changedFilesCoverageRoot = root.filterByChangedFilesCoverage(); - var changeCoverageRoot = root.filterChanges(); + var modifiedFilesCoverageRoot = root.filterByModifiedFilesCoverage(); + var modifiedLinesCoverageRoot = root.filterChanges(); var indirectlyChangedCoverage = root.filterByIndirectlyChangedCoverage(); var projectCoverageHeader = getBulletListItem(1, formatText(TextFormat.BOLD, getUrlText(Baseline.PROJECT_DELTA.getTitle(), getCoverageReportBaseUrl() + Baseline.PROJECT_DELTA.getUrl()))); - var changedFilesCoverageHeader = getBulletListItem(1, + var modifiedFilesCoverageHeader = getBulletListItem(1, formatText(TextFormat.BOLD, getUrlText(Baseline.MODIFIED_FILES_DELTA.getTitle(), getCoverageReportBaseUrl() + Baseline.MODIFIED_FILES_DELTA.getUrl()))); - var changeCoverageHeader = getBulletListItem(1, + var modifiedLinesCoverageHeader = getBulletListItem(1, formatText(TextFormat.BOLD, getUrlText(Baseline.MODIFIED_LINES_DELTA.getTitle(), getCoverageReportBaseUrl() + Baseline.MODIFIED_LINES_DELTA.getUrl()))); var indirectCoverageChangesHeader = getBulletListItem(1, @@ -173,33 +171,34 @@ private String getOverallCoverageSummary(final Node root) { getCoverageReportBaseUrl() + Baseline.INDIRECT.getUrl()))); var projectCoverageLine = getBulletListItem(2, - formatCoverageForMetric(Metric.LINE, action::formatValueForMetric, Baseline.PROJECT)); + formatCoverageForMetric(Metric.LINE, Baseline.PROJECT)); var projectCoverageBranch = getBulletListItem(2, - formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.PROJECT)); - var projectCoverageComplexity = getBulletListItem(2, formatValueOfMetric(root, Metric.COMPLEXITY_DENSITY)); - var projectCoverageLoc = getBulletListItem(2, formatValueOfMetric(root, Metric.LOC)); - - var changedFilesCoverageLine = getBulletListItem(2, - formatCoverageForMetric(Metric.LINE, action::formatValueForMetric, Baseline.MODIFIED_FILES)); - var changedFilesCoverageBranch = getBulletListItem(2, - formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.MODIFIED_FILES)); - var changedFilesCoverageComplexity = getBulletListItem(2, - formatValueOfMetric(changedFilesCoverageRoot, Metric.COMPLEXITY_DENSITY)); - var changedFilesCoverageLoc = getBulletListItem(2, - formatValueOfMetric(changedFilesCoverageRoot, Metric.LOC)); - - var changeCoverageLine = getBulletListItem(2, - formatCoverageForMetric(Metric.LINE, action::formatValueForMetric, Baseline.MODIFIED_LINES)); - var changeCoverageBranch = getBulletListItem(2, - formatCoverageForMetric(Metric.BRANCH, action::formatValueForMetric, Baseline.MODIFIED_LINES)); - var changeCoverageLoc = getBulletListItem(2, formatValueOfMetric(changeCoverageRoot, Metric.LOC)); + 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, - action.formatValueForMetric(Baseline.INDIRECT, Metric.LINE)); + formatCoverageForMetric(Metric.LINE, Baseline.INDIRECT)); var indirectCoverageChangesBranch = getBulletListItem(2, - action.formatValueForMetric(Baseline.INDIRECT, Metric.BRANCH)); + formatCoverageForMetric(Metric.BRANCH, Baseline.INDIRECT)); var indirectCoverageChangesLoc = getBulletListItem(2, - formatValueOfMetric(indirectlyChangedCoverage, Metric.LOC)); + formatRootValueOfMetric(indirectlyChangedCoverage, Metric.LOC)); return sectionHeader + projectCoverageHeader @@ -207,15 +206,15 @@ private String getOverallCoverageSummary(final Node root) { + projectCoverageBranch + projectCoverageComplexity + projectCoverageLoc - + changedFilesCoverageHeader - + changedFilesCoverageLine - + changedFilesCoverageBranch - + changedFilesCoverageComplexity - + changedFilesCoverageLoc - + changeCoverageHeader - + changeCoverageLine - + changeCoverageBranch - + changeCoverageLoc + + modifiedFilesCoverageHeader + + modifiedFilesCoverageLine + + modifiedFilesCoverageBranch + + modifiedFilesCoverageComplexity + + modifiedFilesCoverageLoc + + modifiedLinesCoverageHeader + + modifiedLinesCoverageLine + + modifiedLinesCoverageBranch + + modifiedLinesCoverageLoc + indirectCoverageChangesHeader + indirectCoverageChangesLine + indirectCoverageChangesBranch @@ -235,19 +234,19 @@ private String getHealthReportSummary() { private String getProjectMetricsSummary(final Node result) { String sectionHeader = getSectionHeader(2, Messages.Checks_ProjectOverview()); - List coverageDisplayNames = formatter.getSortedCoverageDisplayNames(); + 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), + 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)); + FORMATTER.getDisplayName(Baseline.PROJECT_DELTA)); Collection projectCoverageDelta = formatCoverageDelta(Metric.getCoverageMetrics(), action.getAllDeltas(Baseline.PROJECT_DELTA)); String projectCoverageDeltaRow = @@ -260,17 +259,15 @@ private String getProjectMetricsSummary(final Node result) { + projectCoverageDeltaRow; } - private String formatCoverageForMetric(final Metric metric, - final BiFunction coverageFormat, - final Baseline baseline) { + private String formatCoverageForMetric(final Metric metric, final Baseline baseline) { return String.format("%s (%s)", - coverageFormat.apply(baseline, metric), action.formatDelta(baseline, metric)); + action.formatValue(baseline, metric), action.formatDelta(baseline, metric)); } - private String formatValueOfMetric(final Node root, final Metric metric) { + private String formatRootValueOfMetric(final Node root, final Metric metric) { var value = root.getValue(metric); - return value.map(action::formatValueWithDetails) - .orElseGet(() -> formatter.getDisplayName(metric) + ": " + Messages.Coverage_Not_Available()); + return value.map(FORMATTER::formatValueWithMetric) + .orElseGet(() -> FORMATTER.getDisplayName(metric) + ": " + Messages.Coverage_Not_Available()); } private String formatText(final TextFormat format, final String text) { @@ -302,12 +299,12 @@ private Collection formatCoverageDelta(final Collection required if (deltas.containsKey(metric)) { var coverage = deltas.get(metric); coverageDelta.putIfAbsent(metric, - formatter.formatDelta(coverage, metric, Functions.getCurrentLocale()) + FORMATTER.formatDelta(coverage, metric, Functions.getCurrentLocale()) + getTrendIcon(coverage.doubleValue())); } else { coverageDelta.putIfAbsent(metric, - formatter.formatPercentage(Coverage.nullObject(metric), Functions.getCurrentLocale())); + FORMATTER.formatPercentage(Coverage.nullObject(metric), Functions.getCurrentLocale())); } } return coverageDelta.values(); diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java index 4fadc518d..f0dad34fd 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentage.java @@ -147,7 +147,7 @@ public String formatPercentage(final Locale locale) { * @return the formatted delta percentage as plain text with a leading sign */ public String formatDeltaPercentage(final Locale locale) { - return String.format(locale, "%+.2f", getDoubleValue()); + return String.format(locale, "%+.2f%%", getDoubleValue()); } public int getNumerator() { 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 7f0e47255..ff4ef2345 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 @@ -60,21 +60,21 @@ void publishAction(final String id, final String optionalName, final String icon log.logInfo("Calculating coverage deltas..."); - Node changeCoverageRoot = rootNode.filterChanges(); - - NavigableMap changeCoverageDelta; - List aggregatedChangedFilesCoverage; - NavigableMap changedFilesCoverageDelta; - if (hasChangeCoverage(changeCoverageRoot)) { - changeCoverageDelta = changeCoverageRoot.computeDelta(rootNode); - Node changedFilesCoverageRoot = rootNode.filterByChangedFilesCoverage(); - aggregatedChangedFilesCoverage = changedFilesCoverageRoot.aggregateValues(); - changedFilesCoverageDelta = changedFilesCoverageRoot.computeDelta(rootNode); + Node modifiedLinesCoverageRoot = rootNode.filterChanges(); + + NavigableMap modifiedLinesCoverageDelta; + List aggregatedModifiedFilesCoverage; + NavigableMap modifiedFilesCoverageDelta; + if (hasModifiedLinesCoverage(modifiedLinesCoverageRoot)) { + modifiedLinesCoverageDelta = modifiedLinesCoverageRoot.computeDelta(rootNode); + Node modifiedFilesCoverageRoot = rootNode.filterByModifiedFilesCoverage(); + aggregatedModifiedFilesCoverage = modifiedFilesCoverageRoot.aggregateValues(); + modifiedFilesCoverageDelta = modifiedFilesCoverageRoot.computeDelta(rootNode); } else { - changeCoverageDelta = new TreeMap<>(); - aggregatedChangedFilesCoverage = new ArrayList<>(); - changedFilesCoverageDelta = new TreeMap<>(); + modifiedLinesCoverageDelta = new TreeMap<>(); + aggregatedModifiedFilesCoverage = new ArrayList<>(); + modifiedFilesCoverageDelta = new TreeMap<>(); if (rootNode.hasChangedLines()) { log.logInfo("No detected code changes affect the code coverage"); } @@ -85,19 +85,19 @@ void publishAction(final String id, final String optionalName, final String icon 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, - aggregatedChangedFilesCoverage, - changedFilesCoverageDelta, + 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 { @@ -148,16 +148,16 @@ 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 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()) { @@ -179,14 +179,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/CoverageViewModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageViewModel.java index 5788b6cb0..634d153b1 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 @@ -67,9 +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 CHANGED_FILES_COVERAGE_TABLE_ID = "changed-files-coverage-table"; + static final String MODIFIED_LINES_COVERAGE_TABLE_ID = "modifies-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"; @@ -85,8 +84,8 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen private final Node node; private final String id; - private final Node changeCoverageTreeRoot; - private final Node changedFilesCoverageTreeRoot; + private final Node modifiedLinesCoverageTreeRoot; + private final Node modifiedFilesCoverageTreeRoot; private final Node indirectCoverageChangesTreeRoot; private final Function trendChartFunction; @@ -111,8 +110,8 @@ 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(); - changedFilesCoverageTreeRoot = node.filterByChangedFilesCoverage(); + modifiedLinesCoverageTreeRoot = node.filterChanges(); + modifiedFilesCoverageTreeRoot = node.filterByModifiedFilesCoverage(); indirectCoverageChangesTreeRoot = node.filterByIndirectlyChangedCoverage(); this.trendChartFunction = trendChartFunction; } @@ -294,11 +293,11 @@ 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 CHANGED_FILES_COVERAGE_TABLE_ID: - return new ChangedFilesCoverageTable(tableId, getNode(), changeCoverageTreeRoot, renderer, + case MODIFIED_FILES_COVERAGE_TABLE_ID: + return new ModifiedFilesCoverageTable(tableId, getNode(), modifiedFilesCoverageTreeRoot, renderer, colorProvider); case INDIRECT_COVERAGE_TABLE_ID: return new IndirectCoverageChangesTable(tableId, getNode(), indirectCoverageChangesTreeRoot, renderer, @@ -389,8 +388,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); @@ -413,11 +412,11 @@ 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() { + public boolean hasModifiedLinesCoverage() { return getNode().getAllFileNodes().stream().anyMatch(FileNode::hasChangedLines); } diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java similarity index 72% rename from plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java rename to plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java index ee6b707af..b7e26e4aa 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangedFilesCoverageTable.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java @@ -17,32 +17,32 @@ import io.jenkins.plugins.datatables.TableConfiguration.SelectStyle; /** - * {@link CoverageTableModel} implementation for visualizing the changed files coverage. + * {@link CoverageTableModel} implementation for visualizing the modified lines coverage. * * @since 4.0.0 */ -class ChangedFilesCoverageTable extends CoverageTableModel { - private final Node changedFilesCoverageRoot; +class ModifiedFilesCoverageTable extends CoverageTableModel { + private final Node modifiedFilesCoverageRoot; /** - * Creates a changed files coverage table model. + * Creates a modified lines coverage table model. * * @param id * The ID of the table * @param root * The root of the origin coverage tree * @param changeRoot - * The root of the change coverage tree + * The root of the Modified Lines Coverage tree * @param renderer * the renderer to use for the file names * @param colorProvider * The {@link ColorProvider} which provides the used colors */ - ChangedFilesCoverageTable(final String id, final Node root, final Node changeRoot, + ModifiedFilesCoverageTable(final String id, final Node root, final Node changeRoot, final RowRenderer renderer, final ColorProvider colorProvider) { super(id, root, renderer, colorProvider); - this.changedFilesCoverageRoot = changeRoot; + this.modifiedFilesCoverageRoot = changeRoot; } @Override @@ -53,8 +53,8 @@ public TableConfiguration getTableConfiguration() { @Override public List getRows() { Locale browserLocale = Functions.getCurrentLocale(); - return changedFilesCoverageRoot.getAllFileNodes().stream() - .map(file -> new ChangedFilesCoverageRow( + return modifiedFilesCoverageRoot.getAllFileNodes().stream() + .map(file -> new ModifiedFilesCoverageRow( getOriginalNode(file), file, browserLocale, getRenderer(), getColorProvider())) .collect(Collectors.toList()); } @@ -68,14 +68,14 @@ private FileNode getOriginalNode(final FileNode fileNode) { } /** - * UI row model for the changed files coverage details table. + * UI row model for the modified lines coverage details table. * * @since 4.0.0 */ - private static class ChangedFilesCoverageRow extends CoverageRow { + private static class ModifiedFilesCoverageRow extends CoverageRow { private final FileNode originalFile; - ChangedFilesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, + ModifiedFilesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { super(changedFileNode, browserLocale, renderer, colorProvider); @@ -84,20 +84,20 @@ private static class ChangedFilesCoverageRow extends CoverageRow { @Override public DetailedCell getLineCoverageDelta() { - return createColoredChangeCoverageDeltaColumn(Metric.LINE); + return createColoredModifiedLinesCoverageDeltaColumn(Metric.LINE); } @Override public DetailedCell getBranchCoverageDelta() { - return createColoredChangeCoverageDeltaColumn(Metric.BRANCH); + return createColoredModifiedLinesCoverageDeltaColumn(Metric.BRANCH); } @Override public int getLoc() { - return getFile().getCoveredLines().size(); + return getFile().getLinesWithCoverage().size(); } - private DetailedCell createColoredChangeCoverageDeltaColumn(final Metric metric) { + private DetailedCell createColoredModifiedLinesCoverageDeltaColumn(final Metric metric) { Coverage fileCoverage = getFile().getTypedValue(metric, Coverage.nullObject(metric)); if (fileCoverage.isSet()) { return createColoredCoverageDeltaColumn(metric, 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/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..8e6897f10 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); } /** 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 index 1e77346b6..abfcd2420 100644 --- 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 @@ -55,7 +55,7 @@ private void assertOutput(final ChecksDetails checkDetails) { assertThat(checkDetails.getOutput()).isPresent().get().satisfies(output -> { assertThat(output.getTitle()).isPresent() .get() - .isEqualTo("Change Coverage: Line: 50.00% (Lines of Code: 323)"); + .isEqualTo("Modified code lines: 50.00% (1/2)"); assertThat(output.getText()).isEmpty(); assertSummary(output); assertChecksAnnotations(output); @@ -109,11 +109,11 @@ private CoverageBuildAction createCoverageBuildAction() { var result = readJacocoResult("jacoco-codingstyle.xml"); result.getAllFileNodes().stream().filter(file -> file.getName().equals("TreeStringBuilder.java")).findFirst() .ifPresent(file -> { - assertThat(file.getCoveredLines()).contains(160, 162, 163, 164); - file.addChangedLine(160); - file.addChangedLine(162); - file.addChangedLine(163); - file.addChangedLine(164); + assertThat(file.getLinesWithCoverage()).contains(160, 162, 163, 164); + file.addModifiedLine(160); + file.addModifiedLine(162); + file.addModifiedLine(163); + file.addModifiedLine(164); }); return new CoverageBuildAction(run, COVERAGE_ID, REPORT_NAME, StringUtils.EMPTY, result, new QualityGateResult() 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 bb4c6d220..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,19 +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/#changedFilesCoverage"); + 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/#changedFilesCoverage"); + assertThat(column.getRelativeCoverageUrl(job)).isEqualTo("coverage/#modifiedFilesCoverage"); column.setBaseline(Baseline.INDIRECT); assertThat(column.getBaseline()).isEqualTo(Baseline.INDIRECT); diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java index ab71e661d..215736efa 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoveragePercentageTest.java @@ -78,7 +78,7 @@ void shouldFormatPercentage() { @Test void shouldFormatDeltaPercentage() { CoveragePercentage coveragePercentage = CoveragePercentage.valueOf(COVERAGE_PERCENTAGE); - assertThat(coveragePercentage.formatDeltaPercentage(LOCALE)).isEqualTo("+50,00"); + assertThat(coveragePercentage.formatDeltaPercentage(LOCALE)).isEqualTo("+50,00%"); } @Test 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 8e6ac77c1..fd006552d 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 @@ -159,7 +159,7 @@ void shouldConvertMetricMap2String() { assertThat(converter.marshal(map)).isEqualTo("[BRANCH: 50/100]"); map.put(LINE, Fraction.getFraction(3, 4)); - assertThat(converter.marshal(map)).isEqualTo("[BRANCH: 50/100, LINE: 3/4]"); + assertThat(converter.marshal(map)).isEqualTo("[LINE: 3/4, BRANCH: 50/100]"); } @Test @@ -170,7 +170,7 @@ void shouldConvertString2MetricMap() { Fraction first = Fraction.getFraction(50, 100); Assertions.assertThat(converter.unmarshal("[BRANCH: 50/100]")) .containsExactly(entry(BRANCH, first)); - Assertions.assertThat(converter.unmarshal("[LINE: 3/4]")) + Assertions.assertThat(converter.unmarshal("[LINE: 3/4, BRANCH: 50/100]")) .containsExactly(entry(LINE, Fraction.getFraction(3, 4)), entry(BRANCH, first)); } 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..57dd1f8af 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,17 +115,17 @@ 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(); 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..ae59067ff 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() + List modifiedFiles = root.getAllFileNodes().stream() .filter(FileNode::hasChangedLines) .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::getChangedLines) .containsExactlyInAnyOrder(15, 17, 63, 68, 80, 90, 130); } From 42a5ee96fd9c8f77520f0b326b987836704b1d8c Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Tue, 14 Feb 2023 23:20:35 +0100 Subject: [PATCH 04/23] Renaming of changes. --- plugin/pom.xml | 2 +- .../plugins/coverage/metrics/steps/CoverageReporter.java | 6 +++--- .../plugins/coverage/metrics/steps/CoverageViewModel.java | 6 +++--- .../plugins/coverage/metrics/steps/GitForensicsITest.java | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 084e10c01..c0082bcb8 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -35,7 +35,7 @@ 1.81 2.9.0 - 0.13.0 + 0.14.0-SNAPSHOT 5.4.0-2-rc759.8b_4e78286216 1.29.0-3-rc207.f000c20b_dea_5 3.0.0-rc679.e40704a_a_f29f 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..56ab8285a 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 @@ -59,7 +59,7 @@ void publishAction(final String id, final String optionalName, final String icon log.logInfo("Calculating coverage deltas..."); - Node changeCoverageRoot = rootNode.filterChanges(); + Node changeCoverageRoot = rootNode.filterByModifiedLines(); NavigableMap changeCoverageDelta; if (hasChangeCoverage(changeCoverageRoot)) { @@ -67,13 +67,13 @@ void publishAction(final String id, final String optionalName, final String icon } else { changeCoverageDelta = new TreeMap<>(); - if (rootNode.hasChangedLines()) { + 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, 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..daf382a9e 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 @@ -109,8 +109,8 @@ 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(); + changeCoverageTreeRoot = node.filterByModifiedLines(); + indirectCoverageChangesTreeRoot = node.filterByIndirectChanges(); this.trendChartFunction = trendChartFunction; } @@ -412,7 +412,7 @@ public boolean hasSourceCode() { * @return {@code true} whether change coverage exists, else {@code false} */ public boolean hasChangeCoverage() { - return getNode().getAllFileNodes().stream().anyMatch(FileNode::hasChangedLines); + return getNode().getAllFileNodes().stream().anyMatch(FileNode::hasModifiedLines); } /** 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..8caf37945 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 @@ -207,7 +207,7 @@ private void verifyCodeDelta(final CoverageBuildAction action) { assertThat(root).isNotNull(); List changedFiles = root.getAllFileNodes().stream() - .filter(FileNode::hasChangedLines) + .filter(FileNode::hasModifiedLines) .collect(Collectors.toList()); assertThat(changedFiles).hasSize(4); assertThat(changedFiles).extracting(FileNode::getName) From c35c60a9abbaa71c97775e28b66562e645ee49b6 Mon Sep 17 00:00:00 2001 From: Florian Orendi Date: Thu, 16 Feb 2023 21:14:32 +0100 Subject: [PATCH 05/23] Fix checks publisher tests --- .../steps/CoverageChecksPublisher.java | 12 +++++-- .../metrics/steps/CoverageRecorder.java | 2 +- .../steps/coverage-publisher-summary.MD | 32 +++++++++---------- 3 files changed, 26 insertions(+), 20 deletions(-) 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 index f9b9dcfd2..ce8c16521 100644 --- 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 @@ -57,7 +57,13 @@ class CoverageChecksPublisher { this.checksName = checksName; } - void publishChecks(final TaskListener listener) { + /** + * 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()); } @@ -228,7 +234,7 @@ private String getOverallCoverageSummary(final Node root) { */ // TODO: expand with summary of status of each defined quality gate private String getHealthReportSummary() { - return getSectionHeader(2, Messages.Checks_QualityGates(action.getQualityGateResult().toString())); + return getSectionHeader(2, Messages.Checks_QualityGates(action.getQualityGateResult().getOverallStatus().name())); } private String getProjectMetricsSummary(final Node result) { @@ -260,7 +266,7 @@ private String getProjectMetricsSummary(final Node result) { } private String formatCoverageForMetric(final Metric metric, final Baseline baseline) { - return String.format("%s (%s)", + return String.format("%s: %s / %s", FORMATTER.getDisplayName(metric), action.formatValue(baseline, metric), action.formatDelta(baseline, metric)); } 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 8a32f223a..1b211467a 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 @@ -361,7 +361,7 @@ void perform(final Run run, final FilePath workspace, final TaskListener t var coverageAction = run.getAction(CoverageBuildAction.class); if (coverageAction != null) { var checksPublisher = new CoverageChecksPublisher(coverageAction, CHECKS_DEFAULT_NAME); - checksPublisher.publishChecks(taskListener); + checksPublisher.publishCoverageReport(taskListener); } } } 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 index af2974155..6659d6c68 100644 --- 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 @@ -1,32 +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: 91.02% (+50.00) - * Branch: 93.97% (n/a) - * Cyclomatic Complexity Density: 0.50% + * Line Coverage: 91.02% (294/323) / +50.00% + * Branch Coverage: 93.97% (109/116) / n/a + * Complexity Density: +49.54% * Lines of Code: 323 -* **[Changed files (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#changedFilesCoverage)** - * Line: 50.00% (+50.00) - * Branch: n/a (n/a) - * Cyclomatic Complexity Density: n/a +* **[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 -* **[Changed code lines (difference to overall project)](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#changeCoverage)** - * Line: 50.00% (+50.00) - * Branch: n/a (n/a) +* **[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: 4 * **[Indirect changes](http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage#indirectCoverage)** - * Line: 50.00% - * Branch: n/a + * Line Coverage: 50.00% (1/2) / n/a + * Branch Coverage: n/a / n/a * Lines of Code: n/a -## Quality Gates Summary - PASSED +## Quality Gates Summary - INACTIVE ## Project Coverage Summary -|Container|Module|Package|File|Class|Method|Branch|Line|Instruction| +|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)|93.97% (109/116)|91.02% (294/323)|93.33% (1260/1350)| -|:chart_with_upwards_trend: **Overall project (difference to reference job)**|-|+20.00 :arrow_up:|-|-|-|-|-|+50.00 :arrow_up:|-| +|: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:|-|-| From eb78397347ba5d9ab4aa6f6a4af294663b08c3dc Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Tue, 28 Feb 2023 23:42:12 +0100 Subject: [PATCH 06/23] Add a toggle to show only changed files in absolute coverage table. --- .../coverage/metrics/steps/CoverageTableModel.java | 9 +++++++++ .../coverage/metrics/steps/CoverageViewModel.java | 8 +------- .../main/resources/coverage/coverage-table.jelly | 14 +++++++++++++- .../metrics/steps/CoverageViewModel/index.jelly | 10 +++++----- plugin/src/main/webapp/js/view-model.js | 8 ++++++++ 5 files changed, 36 insertions(+), 13 deletions(-) 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 7851af459..1fffc162e 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.hasChangedLines(); + } + 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 634d153b1..e17dd5155 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 @@ -67,8 +67,7 @@ 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 MODIFIED_LINES_COVERAGE_TABLE_ID = "modifies-lines-coverage-table"; - static final String MODIFIED_FILES_COVERAGE_TABLE_ID = "modified-files-coverage-table"; + static final String MODIFIED_LINES_COVERAGE_TABLE_ID = "change-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"; @@ -85,7 +84,6 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen private final String id; private final Node modifiedLinesCoverageTreeRoot; - private final Node modifiedFilesCoverageTreeRoot; private final Node indirectCoverageChangesTreeRoot; private final Function trendChartFunction; @@ -111,7 +109,6 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen // initialize filtered coverage trees so that they will not be calculated multiple times modifiedLinesCoverageTreeRoot = node.filterChanges(); - modifiedFilesCoverageTreeRoot = node.filterByModifiedFilesCoverage(); indirectCoverageChangesTreeRoot = node.filterByIndirectlyChangedCoverage(); this.trendChartFunction = trendChartFunction; } @@ -296,9 +293,6 @@ public TableModel getTableModel(final String tableId) { case MODIFIED_LINES_COVERAGE_TABLE_ID: return new ModifiedLinesCoverageTableModel(tableId, getNode(), modifiedLinesCoverageTreeRoot, renderer, colorProvider); - case MODIFIED_FILES_COVERAGE_TABLE_ID: - return new ModifiedFilesCoverageTable(tableId, getNode(), modifiedFilesCoverageTreeRoot, renderer, - colorProvider); case INDIRECT_COVERAGE_TABLE_ID: return new IndirectCoverageChangesTable(tableId, getNode(), indirectCoverageChangesTreeRoot, renderer, colorProvider); 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/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/webapp/js/view-model.js b/plugin/src/main/webapp/js/view-model.js index 3ac3c556d..e8d490539 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(); + }); + }); }); } }; From 24c981507f9abaefd3011fb7147a719e4fa98833 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Wed, 1 Mar 2023 11:20:22 +0100 Subject: [PATCH 07/23] Merge with latest renames in coverage model. --- plugin/pom.xml | 2 +- .../coverage/metrics/steps/CoverageChecksPublisher.java | 8 ++++---- .../plugins/coverage/metrics/steps/CoverageReporter.java | 2 +- .../coverage/metrics/steps/CoverageTableModel.java | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index c0082bcb8..3c0416c57 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -35,7 +35,7 @@ 1.81 2.9.0 - 0.14.0-SNAPSHOT + 0.14.0 5.4.0-2-rc759.8b_4e78286216 1.29.0-3-rc207.f000c20b_dea_5 3.0.0-rc679.e40704a_a_f29f 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 index ce8c16521..06dfcd7a2 100644 --- 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 @@ -100,7 +100,7 @@ private String getSummary() { private List getAnnotations() { var annotations = new ArrayList(); - for (var fileNode : action.getResult().filterChanges().getAllFileNodes()) { + for (var fileNode : action.getResult().filterByModifiedLines().getAllFileNodes()) { for (var aggregatedLines : getAggregatedMissingLines(fileNode)) { ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder() .withPath(fileNode.getPath()) @@ -159,9 +159,9 @@ private String getCoverageReportBaseUrl() { private String getOverallCoverageSummary(final Node root) { String sectionHeader = getSectionHeader(2, Messages.Checks_Summary()); - var modifiedFilesCoverageRoot = root.filterByModifiedFilesCoverage(); - var modifiedLinesCoverageRoot = root.filterChanges(); - var indirectlyChangedCoverage = root.filterByIndirectlyChangedCoverage(); + var modifiedFilesCoverageRoot = root.filterByModifiedFiles(); + var modifiedLinesCoverageRoot = root.filterByModifiedLines(); + var indirectlyChangedCoverage = root.filterByIndirectChanges(); var projectCoverageHeader = getBulletListItem(1, formatText(TextFormat.BOLD, getUrlText(Baseline.PROJECT_DELTA.getTitle(), 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 b0b69b1de..b0dd5e1fc 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 @@ -67,7 +67,7 @@ void publishAction(final String id, final String optionalName, final String icon NavigableMap modifiedFilesCoverageDelta; if (hasModifiedLinesCoverage(modifiedLinesCoverageRoot)) { modifiedLinesCoverageDelta = modifiedLinesCoverageRoot.computeDelta(rootNode); - Node modifiedFilesCoverageRoot = rootNode.filterByModifiedFilesCoverage(); + Node modifiedFilesCoverageRoot = rootNode.filterByModifiedFiles(); aggregatedModifiedFilesCoverage = modifiedFilesCoverageRoot.aggregateValues(); modifiedFilesCoverageDelta = modifiedFilesCoverageRoot.computeDelta(rootNode); } 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 1fffc162e..d398e605c 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 @@ -205,7 +205,7 @@ public String getFileHash() { } public boolean getModified() { - return file.hasChangedLines(); + return file.hasModifiedLines(); } public DetailedCell getFileName() { From f727183910da8b82f22a95c85aca30090431990f Mon Sep 17 00:00:00 2001 From: Ullrich Hafner Date: Wed, 1 Mar 2023 11:24:11 +0100 Subject: [PATCH 08/23] Fix typo. --- .../plugins/coverage/metrics/model/ElementFormatter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fc73491b4..add55c0d4 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 @@ -390,7 +390,7 @@ public List getSortedCoverageDisplayNames() { } /** - * Formats a stream of values to theis display representation by using the passed locale. + * Formats a stream of values to their display representation by using the given locale. * * @param values * The values to be formatted From 8c9777618bc1f74659267f1b0c7daef9c3e55c84 Mon Sep 17 00:00:00 2001 From: Ullrich Hafner Date: Wed, 1 Mar 2023 11:24:25 +0100 Subject: [PATCH 09/23] Fix typo. --- .../plugins/coverage/metrics/source/SourceCodeFacade.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c31518b7e..e98aa3f30 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 Modified Lines Coverage only. + * Filters the sourcecode coverage highlighting for analyzing the modified lines coverage only. * * @param content * The original HTML content From 87196618d2b30444e35c3afdbb01f12717e31f01 Mon Sep 17 00:00:00 2001 From: Ullrich Hafner Date: Wed, 1 Mar 2023 11:24:37 +0100 Subject: [PATCH 10/23] Fix typo. --- .../plugins/coverage/metrics/steps/CoverageViewModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 37889c8f7..c18d53b31 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 @@ -408,7 +408,7 @@ public boolean hasSourceCode() { /** * Checks whether Modified Lines Coverage exists. * - * @return {@code true} whether Modified Lines Coverage exists, else {@code false} + * @return {@code true} whether modified lines coverage exists, else {@code false} */ public boolean hasModifiedLinesCoverage() { return getNode().getAllFileNodes().stream().anyMatch(FileNode::hasModifiedLines); From 9c05d7a93e1db1477115510375f5695f7405ff74 Mon Sep 17 00:00:00 2001 From: Ullrich Hafner Date: Wed, 1 Mar 2023 11:25:00 +0100 Subject: [PATCH 11/23] Remove @since --- .../coverage/metrics/steps/ModifiedFilesCoverageTable.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java index b7e26e4aa..ecc80a159 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java @@ -69,8 +69,6 @@ private FileNode getOriginalNode(final FileNode fileNode) { /** * UI row model for the modified lines coverage details table. - * - * @since 4.0.0 */ private static class ModifiedFilesCoverageRow extends CoverageRow { private final FileNode originalFile; From 5709d5c6f8e9265699d0aa64e4cbbb471c4e0305 Mon Sep 17 00:00:00 2001 From: Ullrich Hafner Date: Wed, 1 Mar 2023 11:25:09 +0100 Subject: [PATCH 12/23] Remove @since --- .../coverage/metrics/steps/ModifiedFilesCoverageTable.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java index ecc80a159..32b6f52ce 100644 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java @@ -18,8 +18,6 @@ /** * {@link CoverageTableModel} implementation for visualizing the modified lines coverage. - * - * @since 4.0.0 */ class ModifiedFilesCoverageTable extends CoverageTableModel { private final Node modifiedFilesCoverageRoot; From 96b4228f746fbfed3a66c179a16e5cf0dc0d71ac Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Wed, 1 Mar 2023 13:03:33 +0100 Subject: [PATCH 13/23] Do not create tree nodes that do not have a value attached. --- .../metrics/charts/TreeMapNodeConverter.java | 12 ++-- .../charts/TreeMapNodeConverterTest.java | 69 ++++++++++--------- 2 files changed, 45 insertions(+), 36 deletions(-) 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/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 From 7eb5c4fb3b7820f98489a798dd5d52c09e970f43 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Wed, 1 Mar 2023 13:06:02 +0100 Subject: [PATCH 14/23] Add incremental versions of dependencies. --- plugin/pom.xml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 3c0416c57..cec65b16a 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -38,8 +38,14 @@ 0.14.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-rc679.e40704a_a_f29f + 5.2.2-1-rc442.6631330fec41 + 6.3.0-1-rc517.f6b_6e5a_dd4ef + + 6.3.0-1-SNAPSHOT @@ -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 From cacc7df433743669714b2412f144b5051124eba9 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Wed, 1 Mar 2023 16:45:08 +0100 Subject: [PATCH 15/23] Add incremental version of forensics-plugin. --- plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index cec65b16a..036944734 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -45,7 +45,7 @@ 5.2.2-1-rc442.6631330fec41 6.3.0-1-rc517.f6b_6e5a_dd4ef - 6.3.0-1-SNAPSHOT + 2.0.0-rc1380.c93c627cd828 From 495ece722521a47b756f53a29393e4e0ed5181ad Mon Sep 17 00:00:00 2001 From: Florian Orendi Date: Wed, 1 Mar 2023 20:51:20 +0100 Subject: [PATCH 16/23] Fix for review comments --- .../metrics/model/ElementFormatter.java | 12 +- .../metrics/steps/CoverageBuildAction.java | 4 +- .../steps/CoverageChecksPublisher.java | 4 +- .../metrics/steps/CoverageRecorder.java | 2 +- .../metrics/steps/CoverageReporter.java | 12 +- .../metrics/steps/CoverageTableModel.java | 6 +- .../metrics/steps/CoverageViewModel.java | 10 +- .../steps/ModifiedFilesCoverageTable.java | 105 ------------------ .../ModifiedFilesCoverageTableModel.java | 40 +++++++ 9 files changed, 68 insertions(+), 127 deletions(-) delete mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java create mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTableModel.java 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 add55c0d4..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,6 +1,5 @@ package io.jenkins.plugins.coverage.metrics.model; -import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.NoSuchElementException; @@ -140,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) { @@ -190,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; } @@ -383,8 +384,6 @@ public String getDisplayName(final Metric metric) { */ public List getSortedCoverageDisplayNames() { return Metric.getCoverageMetrics().stream() - // use the default comparator which uses the enum ordinal - .sorted() .map(this::getDisplayName) .collect(Collectors.toList()); } @@ -417,8 +416,7 @@ public Stream getSortedCoverageValues(final Node coverage) { .map(m -> m.getValueFor(coverage)) .flatMap(Optional::stream) .filter(value -> value instanceof Coverage) - .map(Coverage.class::cast) - .sorted(Comparator.comparing(Coverage::getMetric)); + .map(Coverage.class::cast); } /** 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 59141c13a..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 @@ -412,9 +412,7 @@ public boolean hasValue(final Baseline baseline, final Metric metric) { * @return the formatted value */ public String formatValue(final Baseline baseline, final Metric metric) { - var value = getAllValues(baseline).stream() - .filter(v -> v.getMetric() == metric) - .findFirst(); + var value = getValueForMetric(baseline, metric); return value.isPresent() ? FORMATTER.formatValue(value.get()) : Messages.Coverage_Not_Available(); } 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 index 06dfcd7a2..6ffbc36f5 100644 --- 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 @@ -94,7 +94,7 @@ private String getChecksTitle() { private String getSummary() { var root = action.getResult(); return getOverallCoverageSummary(root) + "\n\n" - + getHealthReportSummary() + "\n\n" + + getQualityGatesSummary() + "\n\n" + getProjectMetricsSummary(root); } @@ -233,7 +233,7 @@ private String getOverallCoverageSummary(final Node root) { * @return the markdown string representing the status summary */ // TODO: expand with summary of status of each defined quality gate - private String getHealthReportSummary() { + private String getQualityGatesSummary() { return getSectionHeader(2, Messages.Checks_QualityGates(action.getQualityGateResult().getOverallStatus().name())); } 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 1b211467a..a465784c3 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,7 +63,7 @@ */ @SuppressWarnings("checkstyle:ClassFanOutComplexity") public class CoverageRecorder extends Recorder { - // TODO: make customizable? + // TODO: make customizable private static final String CHECKS_DEFAULT_NAME = "Code Coverage"; static final String DEFAULT_ID = "coverage"; 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 b0dd5e1fc..fa61d66e4 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 @@ -38,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, + void 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) @@ -148,12 +149,14 @@ 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 Modified Lines 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 modifiedLinesCoverageDistribution, final NavigableMap modifiedLinesCoverageDelta, + final List modifiedLinesCoverageDistribution, + final NavigableMap modifiedLinesCoverageDelta, final NavigableMap coverageDelta, final StageResultHandler resultHandler, final List qualityGates) { var statistics = new CoverageStatistics(rootNode.aggregateValues(), coverageDelta, @@ -169,7 +172,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); } 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 d398e605c..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 @@ -120,7 +120,7 @@ public List getColumns() { .withType(ColumnType.NUMBER) .build(); columns.add(loc); - if (root.getMetrics().contains(Metric.COMPLEXITY)) { + if (root.containsMetric(Metric.COMPLEXITY)) { TableColumn complexity = new ColumnBuilder().withHeaderLabel(Messages.Column_Complexity()) .withDataPropertyKey("complexity") .withResponsivePriority(500) @@ -128,7 +128,7 @@ public List getColumns() { .build(); columns.add(complexity); } - if (root.getMetrics().contains(Metric.COMPLEXITY_DENSITY)) { + if (root.containsMetric(Metric.COMPLEXITY_DENSITY)) { TableColumn complexity = new ColumnBuilder().withHeaderLabel(Messages.Column_ComplexityDensity()) .withDataPropertyKey("density") .withDetailedCell() @@ -142,7 +142,7 @@ public List getColumns() { private void configureValueColumn(final String key, final Metric metric, final String headerLabel, final String deltaHeaderLabel, final List columns) { - if (root.getMetrics().contains(metric)) { + if (root.containsMetric(metric)) { TableColumn lineCoverage = new ColumnBuilder().withHeaderLabel(headerLabel) .withDataPropertyKey(key) .withDetailedCell() 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 c18d53b31..3d37633f6 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 @@ -67,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 MODIFIED_LINES_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,6 +85,7 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen private final String id; private final Node modifiedLinesCoverageTreeRoot; + private final Node modifiedFilesCoverageTreeRoot; private final Node indirectCoverageChangesTreeRoot; private final Function trendChartFunction; @@ -109,6 +111,7 @@ public class CoverageViewModel extends DefaultAsyncTableContentProvider implemen // initialize filtered coverage trees so that they will not be calculated multiple times modifiedLinesCoverageTreeRoot = node.filterByModifiedLines(); + modifiedFilesCoverageTreeRoot = node.filterByModifiedFiles(); indirectCoverageChangesTreeRoot = node.filterByIndirectChanges(); this.trendChartFunction = trendChartFunction; } @@ -293,6 +296,9 @@ public TableModel getTableModel(final String tableId) { case MODIFIED_LINES_COVERAGE_TABLE_ID: return new ModifiedLinesCoverageTableModel(tableId, getNode(), modifiedLinesCoverageTreeRoot, renderer, colorProvider); + case MODIFIED_FILES_COVERAGE_TABLE_ID: + return new ModifiedFilesCoverageTableModel(tableId, getNode(), modifiedFilesCoverageTreeRoot, renderer, + colorProvider); case INDIRECT_COVERAGE_TABLE_ID: return new IndirectCoverageChangesTable(tableId, getNode(), indirectCoverageChangesTreeRoot, renderer, colorProvider); @@ -406,7 +412,7 @@ public boolean hasSourceCode() { } /** - * Checks whether Modified Lines Coverage exists. + * Checks whether modified lines coverage exists. * * @return {@code true} whether modified lines coverage exists, else {@code false} */ diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java deleted file mode 100644 index 32b6f52ce..000000000 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTable.java +++ /dev/null @@ -1,105 +0,0 @@ -package io.jenkins.plugins.coverage.metrics.steps; - -import java.util.List; -import java.util.Locale; -import java.util.stream.Collectors; - -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 hudson.Functions; - -import io.jenkins.plugins.coverage.metrics.color.ColorProvider; -import io.jenkins.plugins.datatables.DetailedCell; -import io.jenkins.plugins.datatables.TableConfiguration; -import io.jenkins.plugins.datatables.TableConfiguration.SelectStyle; - -/** - * {@link CoverageTableModel} implementation for visualizing the modified lines coverage. - */ -class ModifiedFilesCoverageTable extends CoverageTableModel { - private final Node modifiedFilesCoverageRoot; - - /** - * Creates a modified lines coverage table model. - * - * @param id - * The ID of the table - * @param root - * The root of the origin coverage tree - * @param changeRoot - * The root of the Modified Lines Coverage tree - * @param renderer - * the renderer to use for the file names - * @param colorProvider - * The {@link ColorProvider} which provides the used colors - */ - ModifiedFilesCoverageTable(final String id, final Node root, final Node changeRoot, - final RowRenderer renderer, final ColorProvider colorProvider) { - super(id, root, renderer, colorProvider); - - this.modifiedFilesCoverageRoot = changeRoot; - } - - @Override - public TableConfiguration getTableConfiguration() { - return super.getTableConfiguration().select(SelectStyle.SINGLE); - } - - @Override - public List getRows() { - Locale browserLocale = Functions.getCurrentLocale(); - return modifiedFilesCoverageRoot.getAllFileNodes().stream() - .map(file -> new ModifiedFilesCoverageRow( - getOriginalNode(file), file, browserLocale, getRenderer(), getColorProvider())) - .collect(Collectors.toList()); - } - - private FileNode getOriginalNode(final FileNode fileNode) { - return getRoot().getAllFileNodes().stream() - .filter(node -> node.getPath().equals(fileNode.getPath()) - && node.getName().equals(fileNode.getName())) - .findFirst() - .orElse(fileNode); // return this as fallback to prevent exceptions - } - - /** - * UI row model for the modified lines coverage details table. - */ - private static class ModifiedFilesCoverageRow extends CoverageRow { - private final FileNode originalFile; - - ModifiedFilesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, - final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { - super(changedFileNode, browserLocale, renderer, colorProvider); - - this.originalFile = originalFile; - } - - @Override - public DetailedCell getLineCoverageDelta() { - return createColoredModifiedLinesCoverageDeltaColumn(Metric.LINE); - } - - @Override - public DetailedCell getBranchCoverageDelta() { - return createColoredModifiedLinesCoverageDeltaColumn(Metric.BRANCH); - } - - @Override - public int getLoc() { - return getFile().getLinesWithCoverage().size(); - } - - private DetailedCell createColoredModifiedLinesCoverageDeltaColumn(final Metric metric) { - Coverage fileCoverage = getFile().getTypedValue(metric, Coverage.nullObject(metric)); - if (fileCoverage.isSet()) { - return createColoredCoverageDeltaColumn(metric, - fileCoverage.delta(originalFile.getTypedValue(metric, Coverage.nullObject(metric)))); - } - return NO_COVERAGE; - } - } -} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTableModel.java new file mode 100644 index 000000000..b067dbc26 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTableModel.java @@ -0,0 +1,40 @@ +package io.jenkins.plugins.coverage.metrics.steps; + +import java.util.Locale; + +import edu.hm.hafner.metric.FileNode; +import edu.hm.hafner.metric.Node; + +import io.jenkins.plugins.coverage.metrics.color.ColorProvider; + +/** + * A coverage table model that handles the modified files of a change with respect to a result of a reference build. + */ +class ModifiedFilesCoverageTableModel extends ChangesTableModel { + + ModifiedFilesCoverageTableModel(final String id, final Node root, final Node changeRoot, + final RowRenderer renderer, final ColorProvider colorProvider) { + super(id, root, changeRoot, renderer, colorProvider); + } + + @Override + ModifiedFilesCoverageRow createRow(final FileNode file, final Locale browserLocale) { + return new ModifiedFilesCoverageRow(getOriginalNode(file), file, + browserLocale, getRenderer(), getColorProvider()); + } + + /** + * UI row model for the coverage details table of modified files. + */ + private static class ModifiedFilesCoverageRow extends ChangesRow { + ModifiedFilesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, + final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { + super(originalFile, changedFileNode, browserLocale, renderer, colorProvider); + } + + @Override + public int getLoc() { + return getFile().getCoveredLinesOfChangeSet().size(); + } + } +} From 1a6c504c6e747faa19b39caba69fd2f586eb1acb Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Fri, 3 Mar 2023 10:20:18 +0100 Subject: [PATCH 17/23] Use latest incremental version of plugin-util. --- plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 036944734..2f5fb438c 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -41,7 +41,7 @@ 1.11.0 3.6.3-1-rc365.70899fb_d9e1d - 3.0.0-rc679.e40704a_a_f29f + 3.0.0-rc693.c098b_871ea_49 5.2.2-1-rc442.6631330fec41 6.3.0-1-rc517.f6b_6e5a_dd4ef From f41892db7b5fa178b039b15a44711d58583af320 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Fri, 3 Mar 2023 10:59:29 +0100 Subject: [PATCH 18/23] Add customization options for checks. --- .../steps/CoverageChecksPublisher.java | 17 ++- .../metrics/steps/CoverageRecorder.java | 108 +++++++++++++++--- .../metrics/steps/CoverageReporter.java | 3 +- .../coverage/metrics/steps/CoverageStep.java | 54 +++++++++ .../resources/coverage/configuration.jelly | 19 ++- .../coverage/configuration.properties | 2 + .../help-checksAnnotationScope.html | 22 ++++ .../CoverageRecorder/help-checksName.html | 4 + .../help-checksAnnotationScope.html | 22 ++++ .../steps/CoverageStep/help-checksName.html | 4 + .../metrics/steps/Messages.properties | 4 + .../steps/CoverageChecksPublisherTest.java | 68 ++++++----- 12 files changed, 268 insertions(+), 59 deletions(-) create mode 100644 plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksAnnotationScope.html create mode 100644 plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageRecorder/help-checksName.html create mode 100644 plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksAnnotationScope.html create mode 100644 plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/CoverageStep/help-checksName.html 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 index 6ffbc36f5..02d7645f4 100644 --- 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 @@ -29,6 +29,7 @@ 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; @@ -38,23 +39,25 @@ * @author Florian Orendi */ class CoverageChecksPublisher { - - private final ElementFormatter FORMATTER = new ElementFormatter(); + 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) { - this(action, checksName, new JenkinsFacade()); + 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 JenkinsFacade jenkinsFacade) { + final String checksName, final ChecksAnnotationScope annotationScope, final JenkinsFacade jenkinsFacade) { this.jenkinsFacade = jenkinsFacade; this.action = action; this.checksName = checksName; + this.annotationScope = annotationScope; } /** @@ -99,6 +102,10 @@ private String getSummary() { } private List getAnnotations() { + if (annotationScope == ChecksAnnotationScope.SKIP) { + return List.of(); + } + // TODO: check if it makes sense to add annotations for all lines var annotations = new ArrayList(); for (var fileNode : action.getResult().filterByModifiedLines().getAllFileNodes()) { for (var aggregatedLines : getAggregatedMissingLines(fileNode)) { 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 a465784c3..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,8 +63,7 @@ */ @SuppressWarnings("checkstyle:ClassFanOutComplexity") public class CoverageRecorder extends Recorder { - // TODO: make customizable - private static final String CHECKS_DEFAULT_NAME = "Code Coverage"; + static final String CHECKS_DEFAULT_NAME = "Code Coverage"; static final String DEFAULT_ID = "coverage"; private static final ValidationUtilities VALIDATION_UTILITIES = new ValidationUtilities(); @@ -76,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; @@ -190,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. * @@ -345,31 +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); } - if (!skipPublishingChecks) { - var coverageAction = run.getAction(CoverageBuildAction.class); - if (coverageAction != null) { - var checksPublisher = new CoverageChecksPublisher(coverageAction, CHECKS_DEFAULT_NAME); - checksPublisher.publishCoverageReport(taskListener); - } - } } 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) { @@ -481,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. * @@ -539,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 fa61d66e4..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 @@ -38,7 +38,7 @@ */ public class CoverageReporter { @SuppressWarnings("checkstyle:ParameterNumber") - void publishAction(final String id, final String optionalName, final String icon, final Node rootNode, + 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, @@ -124,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, 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/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/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/Messages.properties b/plugin/src/main/resources/io/jenkins/plugins/coverage/metrics/steps/Messages.properties index 2ae9ab514..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 @@ -39,3 +39,7 @@ 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/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java index abfcd2420..bbcf37b57 100644 --- 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 @@ -8,7 +8,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.Fraction; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.DefaultLocale; import edu.hm.hafner.metric.Coverage.CoverageBuilder; @@ -22,6 +23,7 @@ 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; @@ -35,10 +37,11 @@ class CoverageChecksPublisherTest extends AbstractCoverageTest { private static final String COVERAGE_ID = "coverage"; private static final String REPORT_NAME = "Name"; - @Test - void shouldCreate() { - var action = createCoverageBuildAction(); - var publisher = new CoverageChecksPublisher(action, REPORT_NAME, createJenkins()); + @ParameterizedTest(name = "should create checks (skip annotations = {0})") + @ValueSource(booleans = {true, false}) + void shouldCreateChecksReport(final boolean skip) { + var publisher = new CoverageChecksPublisher(createCoverageBuildAction(), REPORT_NAME, + skip ? ChecksAnnotationScope.SKIP : ChecksAnnotationScope.MODIFIED_LINES, createJenkins()); var checkDetails = publisher.extractChecksDetails(); @@ -48,17 +51,17 @@ void shouldCreate() { assertThat(checkDetails.getDetailsURL()).isPresent() .get() .isEqualTo("http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage"); - assertOutput(checkDetails); + assertThatDetailsAreCorrect(checkDetails, skip); } - private void assertOutput(final ChecksDetails checkDetails) { + private void assertThatDetailsAreCorrect(final ChecksDetails checkDetails, final boolean skip) { assertThat(checkDetails.getOutput()).isPresent().get().satisfies(output -> { assertThat(output.getTitle()).isPresent() .get() .isEqualTo("Modified code lines: 50.00% (1/2)"); assertThat(output.getText()).isEmpty(); assertSummary(output); - assertChecksAnnotations(output); + assertChecksAnnotations(output, skip); }); } @@ -69,26 +72,31 @@ private void assertSummary(final ChecksOutput checksOutput) throws IOException { .isEqualTo(expectedContent); } - private void assertChecksAnnotations(final ChecksOutput checksOutput) { - assertThat(checksOutput.getChecksAnnotations()).hasSize(2); - assertThat(checksOutput.getChecksAnnotations().get(0)).satisfies(annotation -> { - assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); - assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); - assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); - assertThat(annotation.getMessage()).isPresent().get() - .isEqualTo("Changed line #L160 is not covered by tests"); - assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(160); - assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(160); - }); - assertThat(checksOutput.getChecksAnnotations().get(1)).satisfies(annotation -> { - assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); - assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); - assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); - assertThat(annotation.getMessage()).isPresent().get() - .isEqualTo("Changed lines #L162 - L164 are not covered by tests"); - assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(162); - assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(164); - }); + private void assertChecksAnnotations(final ChecksOutput checksOutput, final boolean skip) { + if (skip) { + assertThat(checksOutput.getChecksAnnotations()).isEmpty(); + } + else { + assertThat(checksOutput.getChecksAnnotations()).hasSize(2); + assertThat(checksOutput.getChecksAnnotations().get(0)).satisfies(annotation -> { + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).isPresent().get() + .isEqualTo("Changed line #L160 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(160); + assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(160); + }); + assertThat(checksOutput.getChecksAnnotations().get(1)).satisfies(annotation -> { + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).isPresent().get() + .isEqualTo("Changed lines #L162 - L164 are not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(162); + assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(164); + }); + } } private JenkinsFacade createJenkins() { @@ -116,8 +124,8 @@ private CoverageBuildAction createCoverageBuildAction() { file.addModifiedLine(164); }); - return new CoverageBuildAction(run, COVERAGE_ID, REPORT_NAME, StringUtils.EMPTY, result, new QualityGateResult() - , null, "refId", + 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); From af27993fa4ff921fe58ca3329a177f55c705d8e7 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Tue, 7 Mar 2023 13:48:17 +0100 Subject: [PATCH 19/23] Add different types of single lines. - lines covered or not - partially covered lines - lines with survived mutations --- plugin/pom.xml | 2 +- .../metrics/source/SourceCodeFacade.java | 2 +- .../metrics/source/SourceCodePainter.java | 34 ++++-- .../coverage/metrics/source/SourceToHtml.java | 53 +++++---- .../steps/CoverageChecksPublisher.java | 112 +++++++++--------- .../metrics/steps/FileChangesProcessor.java | 4 +- .../metrics/source/SourceCodeFacadeTest.java | 2 +- .../steps/CoverageChecksPublisherTest.java | 38 +++--- .../metrics/steps/DeltaComputationITest.java | 2 +- .../steps/FileChangesProcessorTest.java | 4 +- .../metrics/steps/GitForensicsITest.java | 2 +- ...mmary.MD => coverage-publisher-summary.md} | 2 +- 12 files changed, 143 insertions(+), 114 deletions(-) rename plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/{coverage-publisher-summary.MD => coverage-publisher-summary.md} (98%) diff --git a/plugin/pom.xml b/plugin/pom.xml index 2f5fb438c..5d614c23a 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -35,7 +35,7 @@ 1.81 2.9.0 - 0.14.0 + 0.15.0-SNAPSHOT 5.4.0-2-rc759.8b_4e78286216 1.29.0-3-rc207.f000c20b_dea_5 1.11.0 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 e98aa3f30..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 @@ -174,7 +174,7 @@ File createFileInBuildFolder(final File buildResults, final String id, final Str */ 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/CoverageChecksPublisher.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisher.java index 02d7645f4..5ab38620a 100644 --- 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 @@ -6,6 +6,7 @@ import java.util.List; import java.util.NavigableMap; import java.util.TreeMap; +import java.util.stream.Collectors; import org.apache.commons.lang3.math.Fraction; @@ -105,58 +106,70 @@ private List getAnnotations() { if (annotationScope == ChecksAnnotationScope.SKIP) { return List.of(); } - // TODO: check if it makes sense to add annotations for all lines - var annotations = new ArrayList(); - for (var fileNode : action.getResult().filterByModifiedLines().getAllFileNodes()) { - for (var aggregatedLines : getAggregatedMissingLines(fileNode)) { - ChecksAnnotationBuilder builder = new ChecksAnnotationBuilder() - .withPath(fileNode.getPath()) - .withTitle(Messages.Checks_Annotation_Title()) - .withAnnotationLevel(ChecksAnnotationLevel.WARNING) - .withMessage(getAnnotationMessage(aggregatedLines)) - .withStartLine(aggregatedLines.startLine) - .withEndLine(aggregatedLines.endLine); - - annotations.add(builder.build()); - } + + 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 String getAnnotationMessage(final AggregatedMissingLines aggregatedMissingLines) { - if (aggregatedMissingLines.startLine == aggregatedMissingLines.endLine) { - return Messages.Checks_Annotation_Message_SingleLine(aggregatedMissingLines.startLine); + 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 Messages.Checks_Annotation_Message_MultiLine(aggregatedMissingLines.startLine, - aggregatedMissingLines.endLine); + return String.format("%d mutations survived in line %d", survived, line); } - private List getAggregatedMissingLines(final FileNode fileNode) { - var aggregatedMissingLines = new ArrayList(); - - if (fileNode.hasCoveredLinesInChangeSet()) { - var linesWithCoverage = fileNode.getLinesWithCoverage(); - // there has to be at least one line when it is a file node with changes - var previousLine = linesWithCoverage.first(); - var aggregatedLines = new AggregatedMissingLines(previousLine); - linesWithCoverage.remove(previousLine); - - for (final var line : linesWithCoverage) { - if (line == previousLine + 1) { - aggregatedLines.increaseEndLine(); - } - else { - aggregatedMissingLines.add(aggregatedLines); - aggregatedLines = new AggregatedMissingLines(line); - } - previousLine = 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"; - aggregatedMissingLines.add(aggregatedLines); } + return "Line " + line + " is only partially covered, %d branches are missing."; + } - return aggregatedMissingLines; + private ChecksAnnotationBuilder createAnnotationBuilder(final FileNode fileNode) { + return new ChecksAnnotationBuilder() + .withPath(fileNode.getPath()) + .withAnnotationLevel(ChecksAnnotationLevel.WARNING); } private String getCoverageReportBaseUrl() { @@ -241,7 +254,8 @@ private String getOverallCoverageSummary(final Node root) { */ // TODO: expand with summary of status of each defined quality gate private String getQualityGatesSummary() { - return getSectionHeader(2, Messages.Checks_QualityGates(action.getQualityGateResult().getOverallStatus().name())); + return getSectionHeader(2, + Messages.Checks_QualityGates(action.getQualityGateResult().getOverallStatus().name())); } private String getProjectMetricsSummary(final Node result) { @@ -409,18 +423,4 @@ private enum TextFormat { BOLD, CURSIVE } - - private static class AggregatedMissingLines { - private final int startLine; - private int endLine; - - private AggregatedMissingLines(final int startLine) { - this.startLine = startLine; - this.endLine = startLine; - } - - private void increaseEndLine() { - endLine++; - } - } } 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/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeFacadeTest.java index 8e6897f10..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 @@ -62,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/CoverageChecksPublisherTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/steps/CoverageChecksPublisherTest.java index bbcf37b57..d0e52625b 100644 --- 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 @@ -60,13 +60,13 @@ private void assertThatDetailsAreCorrect(final ChecksDetails checkDetails, final .get() .isEqualTo("Modified code lines: 50.00% (1/2)"); assertThat(output.getText()).isEmpty(); - assertSummary(output); assertChecksAnnotations(output, skip); + assertSummary(output); }); } private void assertSummary(final ChecksOutput checksOutput) throws IOException { - var expectedContent = Files.readString(getResourceAsFile("coverage-publisher-summary.MD")); + var expectedContent = Files.readString(getResourceAsFile("coverage-publisher-summary.md")); assertThat(checksOutput.getSummary()).isPresent() .get() .isEqualTo(expectedContent); @@ -77,24 +77,30 @@ private void assertChecksAnnotations(final ChecksOutput checksOutput, final bool assertThat(checksOutput.getChecksAnnotations()).isEmpty(); } else { - assertThat(checksOutput.getChecksAnnotations()).hasSize(2); + assertThat(checksOutput.getChecksAnnotations()).hasSize(3); assertThat(checksOutput.getChecksAnnotations().get(0)).satisfies(annotation -> { - assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Not covered line"); assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); assertThat(annotation.getMessage()).isPresent().get() - .isEqualTo("Changed line #L160 is not covered by tests"); - assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(160); - assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(160); + .isEqualTo("Line 61 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(61); }); assertThat(checksOutput.getChecksAnnotations().get(1)).satisfies(annotation -> { - assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Missing Coverage"); + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Not covered line"); + assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); + assertThat(annotation.getMessage()).isPresent().get() + .isEqualTo("Line 62 is not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(62); + }); + assertThat(checksOutput.getChecksAnnotations().get(2)).satisfies(annotation -> { + assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Partially covered line"); assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); assertThat(annotation.getMessage()).isPresent().get() - .isEqualTo("Changed lines #L162 - L164 are not covered by tests"); - assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(162); - assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(164); + .isEqualTo("Line 113 is only partially covered, one branch is missing"); + assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(113); }); } } @@ -115,13 +121,11 @@ private CoverageBuildAction createCoverageBuildAction() { var run = mock(Run.class); when(run.getUrl()).thenReturn(BUILD_LINK); var result = readJacocoResult("jacoco-codingstyle.xml"); - result.getAllFileNodes().stream().filter(file -> file.getName().equals("TreeStringBuilder.java")).findFirst() + result.findFile("TreeStringBuilder.java") .ifPresent(file -> { - assertThat(file.getLinesWithCoverage()).contains(160, 162, 163, 164); - file.addModifiedLine(160); - file.addModifiedLine(162); - file.addModifiedLine(163); - file.addModifiedLine(164); + 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, 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 57dd1f8af..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 @@ -128,6 +128,6 @@ private void verifyDeltaComputation(final Run firstBuild, final Run 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 7931d5466..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 @@ -213,7 +213,7 @@ private void verifyCodeDelta(final CoverageBuildAction action) { assertThat(modifiedFiles).extracting(FileNode::getName) .containsExactlyInAnyOrder("MinerFactory.java", "RepositoryMinerStep.java", "SimpleReferenceRecorder.java", "CommitDecoratorFactory.java"); - assertThat(modifiedFiles).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 similarity index 98% rename from plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.MD rename to plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/steps/coverage-publisher-summary.md index 6659d6c68..09f7a2f97 100644 --- 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 @@ -13,7 +13,7 @@ * **[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: 4 + * 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 From afb110ebbb72072fc586eee9802343d7b176098b Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Tue, 7 Mar 2023 13:52:14 +0100 Subject: [PATCH 20/23] Bump version of coverage-model to 0.15.0. --- plugin/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 5d614c23a..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,7 +35,7 @@ 1.81 2.9.0 - 0.15.0-SNAPSHOT + 0.15.0 5.4.0-2-rc759.8b_4e78286216 1.29.0-3-rc207.f000c20b_dea_5 1.11.0 From d7f922e63bc13e40cb6c4b82345f4eed0011a24d Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Tue, 7 Mar 2023 13:54:11 +0100 Subject: [PATCH 21/23] Delete unused table model. --- .../metrics/steps/CoverageViewModel.java | 3 -- .../ModifiedFilesCoverageTableModel.java | 40 ------------------- 2 files changed, 43 deletions(-) delete mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTableModel.java 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 3d37633f6..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 @@ -296,9 +296,6 @@ public TableModel getTableModel(final String tableId) { case MODIFIED_LINES_COVERAGE_TABLE_ID: return new ModifiedLinesCoverageTableModel(tableId, getNode(), modifiedLinesCoverageTreeRoot, renderer, colorProvider); - case MODIFIED_FILES_COVERAGE_TABLE_ID: - return new ModifiedFilesCoverageTableModel(tableId, getNode(), modifiedFilesCoverageTreeRoot, renderer, - colorProvider); case INDIRECT_COVERAGE_TABLE_ID: return new IndirectCoverageChangesTable(tableId, getNode(), indirectCoverageChangesTreeRoot, renderer, colorProvider); diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTableModel.java deleted file mode 100644 index b067dbc26..000000000 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ModifiedFilesCoverageTableModel.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.jenkins.plugins.coverage.metrics.steps; - -import java.util.Locale; - -import edu.hm.hafner.metric.FileNode; -import edu.hm.hafner.metric.Node; - -import io.jenkins.plugins.coverage.metrics.color.ColorProvider; - -/** - * A coverage table model that handles the modified files of a change with respect to a result of a reference build. - */ -class ModifiedFilesCoverageTableModel extends ChangesTableModel { - - ModifiedFilesCoverageTableModel(final String id, final Node root, final Node changeRoot, - final RowRenderer renderer, final ColorProvider colorProvider) { - super(id, root, changeRoot, renderer, colorProvider); - } - - @Override - ModifiedFilesCoverageRow createRow(final FileNode file, final Locale browserLocale) { - return new ModifiedFilesCoverageRow(getOriginalNode(file), file, - browserLocale, getRenderer(), getColorProvider()); - } - - /** - * UI row model for the coverage details table of modified files. - */ - private static class ModifiedFilesCoverageRow extends ChangesRow { - ModifiedFilesCoverageRow(final FileNode originalFile, final FileNode changedFileNode, - final Locale browserLocale, final RowRenderer renderer, final ColorProvider colorProvider) { - super(originalFile, changedFileNode, browserLocale, renderer, colorProvider); - } - - @Override - public int getLoc() { - return getFile().getCoveredLinesOfChangeSet().size(); - } - } -} From 5b483c49171158127a4704daab3a5d266c5c0db4 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Tue, 7 Mar 2023 14:00:36 +0100 Subject: [PATCH 22/23] Simplify assertions. --- .../steps/CoverageChecksPublisherTest.java | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) 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 index d0e52625b..fe0814466 100644 --- 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 @@ -77,31 +77,28 @@ private void assertChecksAnnotations(final ChecksOutput checksOutput, final bool assertThat(checksOutput.getChecksAnnotations()).isEmpty(); } else { - assertThat(checksOutput.getChecksAnnotations()).hasSize(3); - assertThat(checksOutput.getChecksAnnotations().get(0)).satisfies(annotation -> { - assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Not covered line"); - assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); - assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); - assertThat(annotation.getMessage()).isPresent().get() - .isEqualTo("Line 61 is not covered by tests"); - assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(61); - }); - assertThat(checksOutput.getChecksAnnotations().get(1)).satisfies(annotation -> { - assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Not covered line"); - assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); - assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); - assertThat(annotation.getMessage()).isPresent().get() - .isEqualTo("Line 62 is not covered by tests"); - assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(62); - }); - assertThat(checksOutput.getChecksAnnotations().get(2)).satisfies(annotation -> { - assertThat(annotation.getTitle()).isPresent().get().isEqualTo("Partially covered line"); - assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); - assertThat(annotation.getPath()).isPresent().get().isEqualTo("edu/hm/hafner/util/TreeStringBuilder.java"); - assertThat(annotation.getMessage()).isPresent().get() - .isEqualTo("Line 113 is only partially covered, one branch is missing"); - assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(113); - }); + assertThat(checksOutput.getChecksAnnotations()).hasSize(3).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); + }); } } From 377d093b5c69345a626cce97ee8de14cf4e9c0d2 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Tue, 7 Mar 2023 14:13:27 +0100 Subject: [PATCH 23/23] Add test for ALL_LINES. --- .../steps/CoverageChecksPublisherTest.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) 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 index fe0814466..20d5b419d 100644 --- 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 @@ -9,7 +9,7 @@ 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.ValueSource; +import org.junit.jupiter.params.provider.CsvSource; import org.junitpioneer.jupiter.DefaultLocale; import edu.hm.hafner.metric.Coverage.CoverageBuilder; @@ -37,11 +37,10 @@ class CoverageChecksPublisherTest extends AbstractCoverageTest { private static final String COVERAGE_ID = "coverage"; private static final String REPORT_NAME = "Name"; - @ParameterizedTest(name = "should create checks (skip annotations = {0})") - @ValueSource(booleans = {true, false}) - void shouldCreateChecksReport(final boolean skip) { - var publisher = new CoverageChecksPublisher(createCoverageBuildAction(), REPORT_NAME, - skip ? ChecksAnnotationScope.SKIP : ChecksAnnotationScope.MODIFIED_LINES, createJenkins()); + @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(); @@ -51,16 +50,16 @@ void shouldCreateChecksReport(final boolean skip) { assertThat(checkDetails.getDetailsURL()).isPresent() .get() .isEqualTo("http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage"); - assertThatDetailsAreCorrect(checkDetails, skip); + assertThatDetailsAreCorrect(checkDetails, expectedAnnotations); } - private void assertThatDetailsAreCorrect(final ChecksDetails checkDetails, final boolean skip) { + 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, skip); + assertChecksAnnotations(output, expectedAnnotations); assertSummary(output); }); } @@ -72,12 +71,9 @@ private void assertSummary(final ChecksOutput checksOutput) throws IOException { .isEqualTo(expectedContent); } - private void assertChecksAnnotations(final ChecksOutput checksOutput, final boolean skip) { - if (skip) { - assertThat(checksOutput.getChecksAnnotations()).isEmpty(); - } - else { - assertThat(checksOutput.getChecksAnnotations()).hasSize(3).satisfiesExactly( + 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); @@ -100,6 +96,9 @@ private void assertChecksAnnotations(final ChecksOutput checksOutput, final bool assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(113); }); } + else { + assertThat(checksOutput.getChecksAnnotations()).hasSize(expectedAnnotations); + } } private JenkinsFacade createJenkins() {