From b0c829ba4886823d9e2fc853420f476da3da8078 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Fri, 29 Sep 2023 23:09:50 +0200 Subject: [PATCH 1/2] Improve reporting of the GitHub checks annotations. Changes: - Line coverage is now reported as consecutive line ranges. - If a line is not covered (C0 = 0) then mutations are skipped. - Mutator will be shown in the message of an annotation. - In mutation reports line and branch coverage annotations are skipped. --- plugin/pom.xml | 3 +- .../steps/CoverageChecksPublisher.java | 49 ++++++++++++++----- .../steps/CoverageChecksPublisherTest.java | 41 ++++++---------- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 0d8767033..acf1b636a 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -33,7 +33,7 @@ 1.19.0 1.84 - 0.24.0 + 0.25.0 2.0.0 1.29.0-8 1.7.8 @@ -180,6 +180,7 @@ io.jenkins.plugins plugin-util-api + 3.4.0 io.jenkins.plugins 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 1d0556b29..6fa83f612 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 @@ -22,6 +22,7 @@ import edu.hm.hafner.coverage.Mutation; import edu.hm.hafner.coverage.Node; import edu.hm.hafner.coverage.Value; +import edu.hm.hafner.util.LineRange; import edu.hm.hafner.util.VisibleForTesting; import hudson.model.TaskListener; @@ -46,7 +47,7 @@ * * @author Florian Orendi */ -@SuppressWarnings("PMD.GodClass") +@SuppressWarnings({"PMD.GodClass", "PMD.CyclomaticComplexity"}) class CoverageChecksPublisher { private static final ElementFormatter FORMATTER = new ElementFormatter(); private static final int TITLE_HEADER_LEVEL = 4; @@ -245,10 +246,16 @@ private List getAnnotations() { } var annotations = new ArrayList(); - for (var fileNode : filterAnnotations().getAllFileNodes()) { - annotations.addAll(getMissingLines(fileNode)); - annotations.addAll(getPartiallyCoveredLines(fileNode)); - annotations.addAll(getSurvivedMutations(fileNode)); + var filteredByScope = filterAnnotations(); + var hasMutationCoverage = filteredByScope.getValue(Metric.MUTATION).isPresent(); + for (var fileNode : filteredByScope.getAllFileNodes()) { + if (hasMutationCoverage) { + annotations.addAll(getSurvivedMutations(fileNode)); + } + else { + annotations.addAll(getMissingLines(fileNode)); + annotations.addAll(getPartiallyCoveredLines(fileNode)); + } } return annotations; } @@ -263,20 +270,32 @@ private Node filterAnnotations() { } 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) - .withEndLine(line) - .build()) + var builder = createAnnotationBuilder(fileNode); + return fileNode.getMissedLineRanges().stream() + .map(range -> rangeToAnnotation(range, builder)) .collect(Collectors.toList()); } + private ChecksAnnotation rangeToAnnotation(final LineRange range, final ChecksAnnotationBuilder builder) { + if (range.getStart() == range.getEnd()) { + builder.withTitle("Not covered line") + .withMessage(String.format("Line %d is not covered by tests", range.getStart())); + } + else { + builder.withTitle("Not covered lines").withMessage( + String.format("Lines %d-%d are not covered by tests", range.getStart(), range.getEnd())); + } + return builder + .withStartLine(range.getStart()) + .withEndLine(range.getEnd()) + .build(); + } + private Collection getSurvivedMutations(final FileNode fileNode) { var builder = createAnnotationBuilder(fileNode).withTitle("Mutation survived"); return fileNode.getSurvivedMutationsPerLine().entrySet().stream() + .filter(entry -> fileNode.getCoveredOfLine(entry.getKey()) > 0) .map(entry -> builder.withMessage(createMutationMessage(entry.getKey(), entry.getValue())) .withStartLine(entry.getKey()) .withEndLine(entry.getKey()) @@ -293,11 +312,15 @@ private String createMutationDetails(final List mutations) { private String createMutationMessage(final int line, final List survived) { if (survived.size() == 1) { - return "One mutation survived in line " + line; + return String.format("One mutation survived in line %d (%s)", line, formatMutator(survived)); } return String.format("%d mutations survived in line %d", survived.size(), line); } + private String formatMutator(final List survived) { + return survived.get(0).getMutator().replaceAll(".*\\.", ""); + } + private Collection getPartiallyCoveredLines(final FileNode fileNode) { var builder = createAnnotationBuilder(fileNode).withTitle("Partially covered line"); 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 1f50358a1..85d9c6bb8 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 @@ -78,7 +78,7 @@ void shouldShowProjectBaselineForPit() { } @ParameterizedTest(name = "should create checks (scope = {0}, expected annotations = {1})") - @CsvSource({"SKIP, 0", "ALL_LINES, 18", "MODIFIED_LINES, 1"}) + @CsvSource({"SKIP, 0", "ALL_LINES, 5", "MODIFIED_LINES, 1"}) void shouldCreateChecksReportPit(final ChecksAnnotationScope scope, final int expectedAnnotations) { var result = readResult("mutations.xml", new PitestParser()); @@ -101,9 +101,9 @@ private void assertMutationAnnotations(final ChecksOutput output, final int expe assertThat(annotation.getRawDetails()).contains("Survived mutations:\n" + "- Replaced integer addition with subtraction (org.pitest.mutationtest.engine.gregor.mutators.MathMutator)"); assertThat(annotation.getPath()).contains("edu/hm/hafner/coverage/parser/CoberturaParser.java"); - assertThat(annotation.getMessage()).contains("One mutation survived in line 251"); - assertThat(annotation.getStartLine()).isPresent().get().isEqualTo(251); - assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(251); + assertThat(annotation.getMessage()).contains("One mutation survived in line 251 (MathMutator)"); + assertThat(annotation.getStartLine()).isPresent().contains(251); + assertThat(annotation.getEndLine()).isPresent().contains(251); }); } } @@ -111,14 +111,12 @@ private void assertMutationAnnotations(final ChecksOutput output, final int expe private void assertThatTitleIs(final CoverageChecksPublisher publisher, final String expectedTitle) { var checkDetails = publisher.extractChecksDetails(); assertThat(checkDetails.getOutput()).isPresent().get().satisfies(output -> { - assertThat(output.getTitle()).isPresent() - .get() - .isEqualTo(expectedTitle); + assertThat(output.getTitle()).isPresent().contains(expectedTitle); }); } @ParameterizedTest(name = "should create checks (scope = {0}, expected annotations = {1})") - @CsvSource({"SKIP, 0", "ALL_LINES, 36", "MODIFIED_LINES, 3"}) + @CsvSource({"SKIP, 0", "ALL_LINES, 28", "MODIFIED_LINES, 2"}) void shouldCreateChecksReportJaCoCo(final ChecksAnnotationScope scope, final int expectedAnnotations) { var result = readJacocoResult("jacoco-codingstyle.xml"); @@ -126,20 +124,17 @@ void shouldCreateChecksReportJaCoCo(final ChecksAnnotationScope scope, final int var checkDetails = publisher.extractChecksDetails(); - assertThat(checkDetails.getName()).isPresent().get().isEqualTo(REPORT_NAME); + assertThat(checkDetails.getName()).isPresent().contains(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"); + .contains("http://127.0.0.1:8080/job/pipeline-coding-style/job/5/coverage"); assertThatDetailsAreCorrect(checkDetails, expectedAnnotations); } private void assertThatDetailsAreCorrect(final ChecksDetails checkDetails, final int expectedAnnotations) { assertThat(checkDetails.getOutput()).isPresent().get().satisfies(output -> { - assertThat(output.getTitle()).isPresent() - .get() - .isEqualTo("Line Coverage: 50.00% (+50.00%)"); + assertThat(output.getTitle()).isPresent().contains("Line Coverage: 50.00% (+50.00%)"); var expectedDetails = toString("coverage-publisher-details.checks-expected-result"); assertThat(output.getText()).isPresent().get().asString().isEqualToNormalizingWhitespace(expectedDetails); assertChecksAnnotations(output, expectedAnnotations); @@ -160,25 +155,17 @@ private void assertChecksAnnotations(final ChecksOutput checksOutput, final int 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); - assertThat(annotation.getEndLine()).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); - assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(62); + assertThat(annotation.getMessage()).contains("Lines 61-62 are not covered by tests"); + assertThat(annotation.getStartLine()).isPresent().contains(61); + assertThat(annotation.getEndLine()).isPresent().contains(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); - assertThat(annotation.getEndLine()).isPresent().get().isEqualTo(113); + assertThat(annotation.getStartLine()).isPresent().contains(113); + assertThat(annotation.getEndLine()).isPresent().contains(113); }); } else { From e8b730de6870c8b979f0da42a1f65c39e84b8632 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Mon, 2 Oct 2023 09:53:06 +0200 Subject: [PATCH 2/2] Use latest BOM. --- plugin/pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index acf1b636a..4a527ffdb 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -5,7 +5,7 @@ org.jvnet.hudson.plugins analysis-pom - 6.12.0 + 6.14.0 @@ -180,7 +180,6 @@ io.jenkins.plugins plugin-util-api - 3.4.0 io.jenkins.plugins