From 080924e7fbef5b0c217e9818be8b83d0c3da8edf Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Fri, 21 Apr 2023 14:40:08 +0200 Subject: [PATCH 1/2] Add details of a mutation to the checks annotation. --- plugin/pom.xml | 2 +- .../steps/CoverageChecksPublisher.java | 20 ++++++++++--------- .../steps/CoverageChecksPublisherTest.java | 2 ++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index ab37ff830..0ea1bc294 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -32,7 +32,7 @@ 1.18.0 1.83 - 0.23.0 + 0.24.0-rc427.d5fd580c7466 2.0.0 1.29.0-4 1.7.8 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 a36690636..323f15ae1 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 @@ -19,6 +19,7 @@ import edu.hm.hafner.coverage.FileNode; import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Mutation; import edu.hm.hafner.coverage.Node; import edu.hm.hafner.coverage.Value; import edu.hm.hafner.util.VisibleForTesting; @@ -278,15 +279,22 @@ private Collection getSurvivedMutations(final FileNo .map(entry -> builder.withMessage(createMutationMessage(entry.getKey(), entry.getValue())) .withStartLine(entry.getKey()) .withEndLine(entry.getKey()) + .withRawDetails(createMutationDetails(entry.getValue())) .build()) .collect(Collectors.toList()); } - private String createMutationMessage(final int line, final int survived) { - if (survived == 1) { + private String createMutationDetails(final List mutations) { + return mutations.stream() + .map(mutation -> String.format("- %s (%s)", mutation.getDescription(), mutation.getMutator())) + .collect(Collectors.joining("\n", "Survived mutations:\n", "")); + } + + private String createMutationMessage(final int line, final List survived) { + if (survived.size() == 1) { return "One mutation survived in line " + line; } - return String.format("%d mutations survived in line %d", survived, line); + return String.format("%d mutations survived in line %d", survived.size(), line); } private Collection getPartiallyCoveredLines(final FileNode fileNode) { @@ -464,12 +472,6 @@ private ChecksConclusion getCheckConclusion(final QualityGateStatus status) { } } - private enum ColumnAlignment { - CENTER, - LEFT, - RIGHT - } - private enum Icon { FEET(":feet:"), WHITE_CHECK_MARK(":white_check_mark:"), 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 adead503d..bf11d20dc 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 @@ -98,6 +98,8 @@ private void assertMutationAnnotations(final ChecksOutput output, final int expe annotation -> { assertThat(annotation.getTitle()).contains("Mutation survived"); assertThat(annotation.getAnnotationLevel()).isEqualTo(ChecksAnnotationLevel.WARNING); + 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); From 1f1cc4cf28fe548e55f9b82f12f4a344a15e76a0 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Mon, 24 Apr 2023 22:53:27 +0200 Subject: [PATCH 2/2] Rewrite rendering using dedicated classes for each coverage type. Replace all if/else blocks with proper subclassing. This provides the possibility to enhance the source code tooltips for mutation results with additional details for the mutations. --- plugin/pom.xml | 2 +- .../metrics/source/CoverageSourcePrinter.java | 133 ++++++++++++++++++ .../metrics/source/MutationSourcePrinter.java | 124 ++++++++++++++++ .../metrics/source/SourceCodePainter.java | 117 ++++----------- .../coverage/metrics/source/SourceToHtml.java | 118 ---------------- .../steps/CoverageChecksPublisher.java | 4 +- .../metrics/steps/CoverageReporter.java | 2 +- .../source/CoverageSourcePrinterTest.java | 90 ++++++++++++ .../source/MutationSourcePrinterTest.java | 61 ++++++++ .../metrics/source/SourceCodeITest.java | 4 +- .../steps/CoverageChecksPublisherTest.java | 2 +- .../coverage/metrics/source/tooltip.html | 21 +++ 12 files changed, 463 insertions(+), 215 deletions(-) create mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/CoverageSourcePrinter.java create mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/MutationSourcePrinter.java delete mode 100644 plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java create mode 100644 plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/CoverageSourcePrinterTest.java create mode 100644 plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/MutationSourcePrinterTest.java create mode 100644 plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/tooltip.html diff --git a/plugin/pom.xml b/plugin/pom.xml index 0ea1bc294..a7dce2844 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -32,7 +32,7 @@ 1.18.0 1.83 - 0.24.0-rc427.d5fd580c7466 + 0.24.0 2.0.0 1.29.0-4 1.7.8 diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/CoverageSourcePrinter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/CoverageSourcePrinter.java new file mode 100644 index 000000000..e26539583 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/CoverageSourcePrinter.java @@ -0,0 +1,133 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.io.Serializable; +import java.util.Arrays; + +import org.apache.commons.lang3.StringUtils; + +import edu.hm.hafner.coverage.FileNode; + +import io.jenkins.plugins.prism.Sanitizer; + +import static j2html.TagCreator.*; + +/** + * Provides all required information for a {@link FileNode} so that its source code can be rendered together with the + * line and branch coverage in HTML. + */ +class CoverageSourcePrinter implements Serializable { + private static final long serialVersionUID = -6044649044983631852L; + private static final Sanitizer SANITIZER = new Sanitizer(); + + static final String UNDEFINED = "noCover"; + static final String NO_COVERAGE = "coverNone"; + static final String FULL_COVERAGE = "coverFull"; + static final String PARTIAL_COVERAGE = "coverPart"; + private static final String NBSP = " "; + + private final String path; + private final int[] linesToPaint; + private final int[] coveredPerLine; + + private final int[] missedPerLine; + + CoverageSourcePrinter(final FileNode file) { + path = file.getRelativePath(); + + linesToPaint = file.getLinesWithCoverage().stream().mapToInt(i -> i).toArray(); + coveredPerLine = file.getCoveredCounters(); + missedPerLine = file.getMissedCounters(); + } + + public String renderLine(final int line, final String sourceCode) { + var isPainted = isPainted(line); + return tr() + .withClass(isPainted ? getColorClass(line) : CoverageSourcePrinter.UNDEFINED) + .condAttr(isPainted, "data-html-tooltip", isPainted ? getTooltip(line) : StringUtils.EMPTY) + .with( + td().withClass("line") + .with(a().withName(String.valueOf(line)).withText(String.valueOf(line))), + td().withClass("hits") + .with(isPainted ? text(getSummaryColumn(line)) : text(StringUtils.EMPTY)), + td().withClass("code") + .with(rawHtml(SANITIZER.render(cleanupCode(sourceCode))))) + .render(); + } + + private String cleanupCode(final String content) { + return content.replace("\n", StringUtils.EMPTY) + .replace("\r", StringUtils.EMPTY) + .replace(" ", NBSP) + .replace("\t", NBSP.repeat(8)); + } + + final int size() { + return linesToPaint.length; + } + + public String getColorClass(final int line) { + if (getCovered(line) == 0) { + return NO_COVERAGE; + } + else if (getMissed(line) == 0) { + return FULL_COVERAGE; + } + else { + return PARTIAL_COVERAGE; + } + } + + public String getTooltip(final int line) { + var covered = getCovered(line); + var missed = getMissed(line); + if (covered + missed > 1) { + if (missed == 0) { + return "All branches covered"; + } + return String.format("Partially covered, branch coverage: %d/%d", covered, covered + missed); + } + else if (covered == 1) { + return "Covered at least once"; + } + else { + return "Not covered"; + } + } + + public String getSummaryColumn(final int line) { + var covered = getCovered(line); + var missed = getMissed(line); + if (covered + missed > 1) { + return String.format("%d/%d", covered, covered + missed); + } + return String.valueOf(covered); + } + + public final String getPath() { + return path; + } + + public boolean isPainted(final int line) { + return findIndexOfLine(line) >= 0; + } + + int findIndexOfLine(final int line) { + return Arrays.binarySearch(linesToPaint, line); + } + + public int getCovered(final int line) { + return getCounter(line, coveredPerLine); + } + + public int getMissed(final int line) { + return getCounter(line, missedPerLine); + } + + int getCounter(final int line, final int... counters) { + var index = findIndexOfLine(line); + if (index >= 0) { + return counters[index]; + } + return 0; + } +} diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/MutationSourcePrinter.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/MutationSourcePrinter.java new file mode 100644 index 000000000..09c6c1947 --- /dev/null +++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/MutationSourcePrinter.java @@ -0,0 +1,124 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import edu.hm.hafner.coverage.FileNode; +import edu.hm.hafner.coverage.Mutation; + +import j2html.tags.ContainerTag; +import j2html.tags.UnescapedText; + +import static j2html.TagCreator.*; + +/** + * Provides all required information for a {@link FileNode} so that its source code can be rendered together with the + * line and mutation coverage in HTML. + */ +class MutationSourcePrinter extends CoverageSourcePrinter { + private static final long serialVersionUID = -2215657894423024907L; + + private final int[] survivedPerLine; + private final int[] killedPerLine; + private final String[] tooltipPerLine; + + MutationSourcePrinter(final FileNode file) { + super(file); + + survivedPerLine = new int[size()]; + killedPerLine = new int[size()]; + tooltipPerLine = new String[size()]; + Arrays.fill(tooltipPerLine, StringUtils.EMPTY); + + extractMutationDetails(file.getMutationsPerLine()); + + for (Mutation mutation : file.getMutations()) { + if (mutation.hasSurvived()) { + survivedPerLine[findIndexOfLine(mutation.getLine())]++; + } + else if (mutation.isKilled()) { + killedPerLine[findIndexOfLine(mutation.getLine())]++; + } + } + } + + private void extractMutationDetails(final NavigableMap> mutationsPerLine) { + for (Entry> entry : mutationsPerLine.entrySet()) { + var indexOfLine = findIndexOfLine(entry.getKey()); + + tooltipPerLine[indexOfLine] = createInfo(entry.getValue()); + } + } + + private String createInfo(final List allMutations) { + ContainerTag killedContainer = listMutations(allMutations, + Mutation::isKilled, "Killed Mutations:"); + ContainerTag survivedContainer = listMutations(allMutations, + Mutation::hasSurvived, "Survived Mutations:"); + if (killedContainer.getNumChildren() == 0 && survivedContainer.getNumChildren() == 0) { + return "Not covered"; + } + return div().with(killedContainer, survivedContainer).render(); + } + + private ContainerTag listMutations(final List allMutations, + final Predicate predicate, final String title) { + var filtered = div(); + var killed = asBulletPoints(allMutations, predicate); + if (!killed.isEmpty()) { + filtered.with(div().with(new UnescapedText(title), ul().with(killed))); + } + return filtered; + } + + private List asBulletPoints(final List mutations, final Predicate predicate) { + return mutations.stream().filter(predicate).map(mutation -> + li().withText(String.format("%s (%s)", mutation.getDescription(), mutation.getMutator()))) + .collect(Collectors.toList()); + } + + public int getSurvived(final int line) { + return getCounter(line, survivedPerLine); + } + + public int getKilled(final int line) { + return getCounter(line, killedPerLine); + } + + @Override + public String getColorClass(final int line) { + if (getCovered(line) == 0) { + return NO_COVERAGE; + } + if (getKilled(line) == 0) { + return NO_COVERAGE; + } + else if (getSurvived(line) == 0) { + return FULL_COVERAGE; + } + else { + return PARTIAL_COVERAGE; + } + } + + @Override + public String getTooltip(final int line) { + return StringUtils.defaultIfBlank(tooltipPerLine[findIndexOfLine(line)], super.getTooltip(line)); + } + + @Override + public String getSummaryColumn(final int line) { + var killed = getKilled(line); + var survived = getSurvived(line); + if (survived + killed > 0) { + return String.format("%d/%d", killed, survived + killed); + } + return String.valueOf(killed); + } +} 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 a1277027e..842cbcef7 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 @@ -3,13 +3,11 @@ import java.io.BufferedWriter; import java.io.File; import java.io.IOException; -import java.io.Serializable; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -17,7 +15,8 @@ import org.apache.commons.io.FileUtils; import edu.hm.hafner.coverage.FileNode; -import edu.hm.hafner.coverage.Mutation; +import edu.hm.hafner.coverage.Metric; +import edu.hm.hafner.coverage.Node; import edu.hm.hafner.util.FilteredLog; import edu.umd.cs.findbugs.annotations.NonNull; @@ -57,6 +56,8 @@ public SourceCodePainter(@NonNull final Run build, @NonNull final FilePath /** * Processes the source code painting. * + * @param rootNode + * the root of the tree * @param files * the files to paint * @param sourceCodeEncoding @@ -69,14 +70,13 @@ public SourceCodePainter(@NonNull final Run build, @NonNull final FilePath * @throws InterruptedException * if the painting process has been interrupted */ - public void processSourceCodePainting(final List files, - final String sourceCodeEncoding, - final SourceCodeRetention sourceCodeRetention, final FilteredLog log) + public void processSourceCodePainting(final Node rootNode, final List files, + final String sourceCodeEncoding, final SourceCodeRetention sourceCodeRetention, final FilteredLog log) throws InterruptedException { SourceCodeFacade sourceCodeFacade = new SourceCodeFacade(); if (sourceCodeRetention != SourceCodeRetention.NEVER) { var paintedFiles = files.stream() - .map(PaintedNode::new) + .map(f -> createFileModel(rootNode, f)) .collect(Collectors.toList()); log.logInfo("Painting %d source files on agent", paintedFiles.size()); @@ -88,7 +88,16 @@ public void processSourceCodePainting(final List files, sourceCodeRetention.cleanup(build, sourceCodeFacade.getCoverageSourcesDirectory(), log); } - private void paintFilesOnAgent(final List paintedFiles, + private CoverageSourcePrinter createFileModel(final Node rootNode, final FileNode fileNode) { + if (rootNode.getValue(Metric.MUTATION).isPresent()) { + return new MutationSourcePrinter(fileNode); + } + else { + return new CoverageSourcePrinter(fileNode); + } + } + + private void paintFilesOnAgent(final List paintedFiles, final String sourceCodeEncoding, final FilteredLog log) throws InterruptedException { try { var painter = new AgentCoveragePainter(paintedFiles, sourceCodeEncoding, id); @@ -108,25 +117,25 @@ private void paintFilesOnAgent(final List paintedFiles, static class AgentCoveragePainter extends MasterToSlaveFileCallable { private static final long serialVersionUID = 3966282357309568323L; - private final List paintedFiles; + private final List paintedFiles; private final String sourceCodeEncoding; private final String directory; /** * Creates a new instance of {@link AgentCoveragePainter}. * - * @param paintedFiles - * the model for the file painting for each coverage node + * @param files + * the pretty printers for the files to create the HTML reports for * @param sourceCodeEncoding * the encoding of the source code files * @param directory * the subdirectory where the source files will be stored in */ - AgentCoveragePainter(final List paintedFiles, final String sourceCodeEncoding, + AgentCoveragePainter(final List files, final String sourceCodeEncoding, final String directory) { super(); - this.paintedFiles = paintedFiles; + this.paintedFiles = files; this.sourceCodeEncoding = sourceCodeEncoding; this.directory = directory; } @@ -176,7 +185,7 @@ private Charset getCharset() { return new ValidationUtilities().getCharset(sourceCodeEncoding); } - private int paintSource(final PaintedNode fileNode, final FilePath workspace, + private int paintSource(final CoverageSourcePrinter fileNode, final FilePath workspace, final Path temporaryFolder, final FilteredLog log) { String relativePathIdentifier = fileNode.getPath(); FilePath paintedFilesDirectory = workspace.child(directory); @@ -186,7 +195,7 @@ paintedFilesDirectory, temporaryFolder, getCharset(), log)) .orElse(0); } - private int paint(final PaintedNode paint, final String relativePathIdentifier, + private int paint(final CoverageSourcePrinter paint, final String relativePathIdentifier, final FilePath resolvedPath, final FilePath paintedFilesDirectory, final Path temporaryFolder, final Charset charset, final FilteredLog log) { String sanitizedFileName = SourceCodeFacade.sanitizeFilename(relativePathIdentifier); @@ -197,7 +206,9 @@ private int paint(final PaintedNode paint, final String relativePathIdentifier, Path fullSourcePath = paintedFilesFolder.resolve(sanitizedFileName); try (BufferedWriter output = Files.newBufferedWriter(fullSourcePath)) { List lines = Files.readAllLines(Paths.get(resolvedPath.getRemote()), charset); - new SourceToHtml().paintSourceCodeWithCoverageInformation(paint, output, lines); + for (int line = 0; line < lines.size(); line++) { + output.write(paint.renderLine(line + 1, lines.get(line))); + } } new FilePath(fullSourcePath.toFile()).zip(zipOutputPath); FileUtils.deleteDirectory(paintedFilesFolder.toFile()); @@ -250,78 +261,4 @@ private void deleteFolder(final File folder, final FilteredLog log) { } } - /** - * Provides all required information for a {@link FileNode} so that its source code can be rendered in HTML. - */ - static class PaintedNode implements Serializable { - private static final long serialVersionUID = -6044649044983631852L; - - private enum Type { - MUTATION, - COVERAGE - } - - private final String path; - private final int[] linesToPaint; - private final int[] coveredPerLine; - private final int[] missedPerLine; - private final int[] survivedPerLine; - private final int[] killedPerLine; - - PaintedNode(final FileNode file) { - path = file.getRelativePath(); - - linesToPaint = file.getLinesWithCoverage().stream().mapToInt(i -> i).toArray(); - coveredPerLine = file.getCoveredCounters(); - missedPerLine = file.getMissedCounters(); - - survivedPerLine = new int[linesToPaint.length]; - killedPerLine = new int[linesToPaint.length]; - - for (Mutation mutation : file.getMutations()) { - if (mutation.hasSurvived()) { - survivedPerLine[findLine(mutation.getLine())]++; - } - else if (mutation.isKilled()) { - killedPerLine[findLine(mutation.getLine())]++; - } - } - } - - public String getPath() { - return path; - } - - public boolean isPainted(final int line) { - return findLine(line) >= 0; - } - - private int findLine(final int line) { - return Arrays.binarySearch(linesToPaint, line); - } - - public int getCovered(final int line) { - return getCounter(line, coveredPerLine); - } - - 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) { - return counters[index]; - } - return 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 deleted file mode 100644 index 79540c9a5..000000000 --- a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceToHtml.java +++ /dev/null @@ -1,118 +0,0 @@ -package io.jenkins.plugins.coverage.metrics.source; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; - -import io.jenkins.plugins.coverage.metrics.source.SourceCodePainter.PaintedNode; - -/** - * Converts a source code file into an HTML document. The HTML document contains the source code with highlighted - * coverage information. - * - * @author Ullrich Hafner - */ -public class SourceToHtml { - void paintSourceCodeWithCoverageInformation(final PaintedNode paint, final BufferedWriter output, final List lines) - throws IOException { - for (int line = 0; line < lines.size(); line++) { - paintLine(line + 1, lines.get(line), paint, output); - } - } - - // TODO rewrite using j2html and prism.js - private void paintLine(final int line, final String content, final PaintedNode paint, - final BufferedWriter output) throws IOException { - if (paint.isPainted(line)) { - int covered = paint.getCovered(line); - int missed = paint.getMissed(line); - - int survived = paint.getSurvived(line); - int killed = paint.getKilled(line); - - output.write("\n"); - output.write("" + line + "\n"); - - String display; - - 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"); - - } - else { - output.write("\n"); - output.write("" + line + "\n"); - output.write("\n"); - } - output.write("" - + content.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\n", "") - .replace("\r", "") - .replace(" ", - " ") - .replace("\t", "        ") + "\n"); - output.write("\n"); - } - - 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 && survived == 0) { - return "coverFull"; - } - else { - return "coverPart"; - } - } - - private String getTooltip(final int missed, final int covered, final int survived, final int killed) { - var tooltip = getTooltipValue(missed, covered, survived, killed); - if (StringUtils.isBlank(tooltip)) { - return StringUtils.EMPTY; - } - return "tooltip=\"" + tooltip + "\""; - } - - // TODO: Extract into classes so that we can paint the mutations as well - private String getTooltipValue(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 "All branches covered"; - } - return String.format("Partially covered, branch coverage: %d/%d", covered, covered + missed); - } - else if (covered == 1) { - return "Covered at least once"; - } - else { - 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 323f15ae1..0cbb6ca34 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 @@ -214,7 +214,7 @@ private void createMutationCoverageSummary(final Node filteredRoot, final List getMissingLines(final FileNode fi private Collection getSurvivedMutations(final FileNode fileNode) { var builder = createAnnotationBuilder(fileNode).withTitle("Mutation survived"); - return fileNode.getSurvivedMutations().entrySet().stream() + return fileNode.getSurvivedMutationsPerLine().entrySet().stream() .map(entry -> builder.withMessage(createMutationMessage(entry.getKey(), entry.getValue())) .withStartLine(entry.getKey()) .withEndLine(entry.getKey()) 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 04261ea98..7a09e3651 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 @@ -112,7 +112,7 @@ CoverageBuildAction publishAction(final String id, final String optionalName, fi log.logInfo("Executing source code painting..."); SourceCodePainter sourceCodePainter = new SourceCodePainter(build, workspace, id); - sourceCodePainter.processSourceCodePainting(filesToStore, + sourceCodePainter.processSourceCodePainting(rootNode, filesToStore, sourceCodeEncoding, sourceCodeRetention, log); log.logInfo("Finished coverage processing - adding the action to the build..."); diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/CoverageSourcePrinterTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/CoverageSourcePrinterTest.java new file mode 100644 index 000000000..e53a90377 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/CoverageSourcePrinterTest.java @@ -0,0 +1,90 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.xmlunit.assertj.XmlAssert; + +import edu.hm.hafner.coverage.parser.JacocoParser; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; + +import static org.assertj.core.api.Assertions.*; + +class CoverageSourcePrinterTest extends AbstractCoverageTest { + static final String CLASS = "class"; + static final String RENDERED_CODE = "                    " + + "for (int line = 0; line < lines.size(); line++) {"; + + @Test + void shouldRenderLinesWithVariousCoverages() { + var tree = readResult("../steps/jacoco-codingstyle.xml", new JacocoParser()); + + var file = new CoverageSourcePrinter(tree.findFile("TreeStringBuilder.java").get()); + + assertThat(file.getColorClass(0)).isEqualTo(CoverageSourcePrinter.NO_COVERAGE); + assertThat(file.getSummaryColumn(0)).isEqualTo("0"); + assertThat(file.getTooltip(0)).isEqualTo("Not covered"); + + assertThat(file.getColorClass(113)).isEqualTo(CoverageSourcePrinter.PARTIAL_COVERAGE); + assertThat(file.getSummaryColumn(113)).isEqualTo("1/2"); + assertThat(file.getTooltip(113)).isEqualToIgnoringWhitespace("Partially covered, branch coverage: 1/2"); + + assertThat(file.getColorClass(61)).isEqualTo(CoverageSourcePrinter.NO_COVERAGE); + assertThat(file.getSummaryColumn(61)).isEqualTo("0"); + assertThat(file.getTooltip(61)).isEqualTo("Not covered"); + + assertThat(file.getColorClass(19)).isEqualTo(CoverageSourcePrinter.FULL_COVERAGE); + assertThat(file.getSummaryColumn(19)).isEqualTo("1"); + assertThat(file.getTooltip(19)).isEqualTo("Covered at least once"); + + var anotherFile = new CoverageSourcePrinter(tree.findFile("StringContainsUtils.java").get()); + + assertThat(anotherFile.getColorClass(43)).isEqualTo(CoverageSourcePrinter.FULL_COVERAGE); + assertThat(anotherFile.getSummaryColumn(43)).isEqualTo("2/2"); + assertThat(anotherFile.getTooltip(43)).isEqualTo("All branches covered"); + } + + @Test + void shouldRenderWholeLine() { + var tree = readResult("../steps/jacoco-codingstyle.xml", new JacocoParser()); + + var file = new CoverageSourcePrinter(tree.findFile("TreeStringBuilder.java").get()); + + var renderedLine = file.renderLine(61, + " for (int line = 0; line < lines.size(); line++) {\n"); + + XmlAssert.assertThat(renderedLine) + .nodesByXPath("/tr").exist().hasSize(1) + .singleElement() + .hasAttribute(CLASS, CoverageSourcePrinter.NO_COVERAGE) + .hasAttribute("data-html-tooltip", "Not covered"); + var assertThatColumns = XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td").exist().hasSize(3); + assertThatColumns.extractingAttribute("class").containsExactly("line", "hits", "code"); + + XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td[1]/a").exist().hasSize(1) + .extractingAttribute("name").containsExactly("61"); + XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td[2]") + .extractingText().containsExactly("0"); + XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td[3]") + .extractingText().containsExactly(RENDERED_CODE); + + var skippedLine = file.renderLine(1, "package io.jenkins.plugins.coverage.metrics.source;"); + + var assertThatSkippedColumns = XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td").exist().hasSize(3); + assertThatSkippedColumns.extractingAttribute("class").containsExactly("line", "hits", "code"); + + XmlAssert.assertThat(skippedLine) + .nodesByXPath("/tr").exist().hasSize(1) + .singleElement() + .hasAttribute(CLASS, CoverageSourcePrinter.UNDEFINED) + .doesNotHaveAttribute("data-html-tooltip"); + + XmlAssert.assertThat(skippedLine).nodesByXPath("/tr/td[1]/a").exist().hasSize(1) + .extractingAttribute("name").containsExactly("1"); + XmlAssert.assertThat(skippedLine).nodesByXPath("/tr/td[2]") + .extractingText().containsExactly(StringUtils.EMPTY); + XmlAssert.assertThat(skippedLine).nodesByXPath("/tr/td[3]") + .extractingText().containsExactly("package io.jenkins.plugins.coverage.metrics.source;"); + + } +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/MutationSourcePrinterTest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/MutationSourcePrinterTest.java new file mode 100644 index 000000000..8e72b6552 --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/MutationSourcePrinterTest.java @@ -0,0 +1,61 @@ +package io.jenkins.plugins.coverage.metrics.source; + +import org.junit.jupiter.api.Test; +import org.xmlunit.assertj.XmlAssert; + +import edu.hm.hafner.coverage.parser.PitestParser; + +import io.jenkins.plugins.coverage.metrics.AbstractCoverageTest; + +import static org.assertj.core.api.Assertions.*; + +class MutationSourcePrinterTest extends AbstractCoverageTest { + @Test + void shouldRenderLinesWithVariousMutations() { + var tree = readResult("../steps/mutations.xml", new PitestParser()); + + var file = new MutationSourcePrinter(tree.findFile("CoberturaParser.java").get()); + + assertThat(file.getColorClass(251)).isEqualTo(CoverageSourcePrinter.PARTIAL_COVERAGE); + assertThat(file.getSummaryColumn(251)).isEqualTo("2/3"); + assertThat(file.getTooltip(251)).isEqualToIgnoringWhitespace(toString("tooltip.html")); + + assertThat(file.getColorClass(254)).isEqualTo(CoverageSourcePrinter.NO_COVERAGE); + assertThat(file.getSummaryColumn(254)).isEqualTo("0"); + assertThat(file.getTooltip(254)).isEqualTo("Not covered"); + + assertThat(file.getColorClass(131)).isEqualTo(CoverageSourcePrinter.FULL_COVERAGE); + assertThat(file.getSummaryColumn(131)).isEqualTo("1/1"); + assertThat(file.getTooltip(131)).contains("Killed Mutations:
  • Replaced integer subtraction with addition (org.pitest.mutationtest.engine.gregor.mutators.MathMutator)
"); + + assertThat(file.getColorClass(137)).isEqualTo(CoverageSourcePrinter.NO_COVERAGE); + assertThat(file.getSummaryColumn(137)).isEqualTo("0/1"); + assertThat(file.getTooltip(137)).contains("Survived Mutations:
  • negated conditional (org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator)
"); + } + + @Test + void shouldRenderWholeLine() { + var tree = readResult("../steps/mutations.xml", new PitestParser()); + + var file = new MutationSourcePrinter(tree.findFile("CoberturaParser.java").get()); + + var renderedLine = file.renderLine(137, + " for (int line = 0; line < lines.size(); line++) {\n"); + + XmlAssert.assertThat(renderedLine) + .nodesByXPath("/tr").exist().hasSize(1) + .singleElement() + .hasAttribute(CoverageSourcePrinterTest.CLASS, "coverNone") + .hasAttribute("data-html-tooltip"); + var assertThatColumns = XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td").exist().hasSize(3); + assertThatColumns.extractingAttribute("class").containsExactly("line", "hits", "code"); + + XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td[1]/a").exist().hasSize(1) + .extractingAttribute("name").containsExactly("137"); + XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td[2]") + .extractingText().containsExactly("0/1"); + XmlAssert.assertThat(renderedLine).nodesByXPath("/tr/td[3]") + .extractingText().containsExactly(CoverageSourcePrinterTest.RENDERED_CODE); + } + +} diff --git a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeITest.java b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeITest.java index c176e64bc..5efaea627 100644 --- a/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/coverage/metrics/source/SourceCodeITest.java @@ -34,8 +34,8 @@ * @author Ullrich Hafner */ abstract class SourceCodeITest extends AbstractCoverageITest { - private static final String ACU_COBOL_PARSER = "public class AcuCobolParser extends LookaheadParser {"; - private static final String PATH_UTIL = "public class PathUtil {"; + private static final String ACU_COBOL_PARSER = "271        super(ACU_COBOL_WARNING_PATTERN);"; + private static final String PATH_UTIL = "201public class PathUtil {"; private static final String NO_SOURCE_CODE = "n/a"; static final String ACU_COBOL_PARSER_FILE_NAME = "AcuCobolParser.java"; static final String ACU_COBOL_PARSER_SOURCE_FILE = ACU_COBOL_PARSER_FILE_NAME + ".txt"; 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 bf11d20dc..c61940243 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 @@ -210,7 +210,7 @@ private CoverageBuildAction createCoverageBuildAction(final Node result) { }); result.findFile("CoberturaParser.java") .ifPresent(file -> { - assertThat(file.getSurvivedMutations()).containsKey(251); + assertThat(file.getSurvivedMutationsPerLine()).containsKey(251); file.addModifiedLines(251); }); diff --git a/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/tooltip.html b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/tooltip.html new file mode 100644 index 000000000..d0cc63f4e --- /dev/null +++ b/plugin/src/test/resources/io/jenkins/plugins/coverage/metrics/source/tooltip.html @@ -0,0 +1,21 @@ +
+
+
Killed Mutations: +
    +
  • changed conditional boundary + (org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator) +
  • +
  • negated conditional (org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator)
  • +
+
+
+
+
Survived Mutations: +
    +
  • Replaced integer addition with subtraction + (org.pitest.mutationtest.engine.gregor.mutators.MathMutator) +
  • +
+
+
+