Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.

Commit 982a2a7

Browse files
committed
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.
1 parent 080924e commit 982a2a7

File tree

12 files changed

+459
-215
lines changed

12 files changed

+459
-215
lines changed

plugin/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<testcontainers.version>1.18.0</testcontainers.version>
3333
<job-dsl.version>1.83</job-dsl.version>
3434

35-
<coverage-model.version>0.24.0-rc427.d5fd580c7466</coverage-model.version>
35+
<coverage-model.version>0.24.0</coverage-model.version>
3636
<git-forensics.version>2.0.0</git-forensics.version>
3737
<prism-api.version>1.29.0-4</prism-api.version>
3838
<pull-request-monitoring.version>1.7.8</pull-request-monitoring.version>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package io.jenkins.plugins.coverage.metrics.source;
2+
3+
import java.io.Serializable;
4+
import java.util.Arrays;
5+
6+
import org.apache.commons.lang3.StringUtils;
7+
8+
import edu.hm.hafner.coverage.FileNode;
9+
10+
import io.jenkins.plugins.prism.Sanitizer;
11+
12+
import static j2html.TagCreator.*;
13+
14+
/**
15+
* Provides all required information for a {@link FileNode} so that its source code can be rendered together with the
16+
* line and branch coverage in HTML.
17+
*/
18+
class CoverageSourcePrinter implements Serializable {
19+
private static final long serialVersionUID = -6044649044983631852L;
20+
private static final Sanitizer SANITIZER = new Sanitizer();
21+
22+
static final String UNDEFINED = "noCover";
23+
static final String NO_COVERAGE = "coverNone";
24+
static final String FULL_COVERAGE = "coverFull";
25+
static final String PARTIAL_COVERAGE = "coverPart";
26+
private static final String NBSP = "&nbsp;";
27+
28+
private final String path;
29+
private final int[] linesToPaint;
30+
private final int[] coveredPerLine;
31+
32+
private final int[] missedPerLine;
33+
34+
CoverageSourcePrinter(final FileNode file) {
35+
path = file.getRelativePath();
36+
37+
linesToPaint = file.getLinesWithCoverage().stream().mapToInt(i -> i).toArray();
38+
coveredPerLine = file.getCoveredCounters();
39+
missedPerLine = file.getMissedCounters();
40+
}
41+
42+
public String renderLine(final int line, final String sourceCode) {
43+
var isPainted = isPainted(line);
44+
return tr()
45+
.withClass(isPainted ? getColorClass(line) : CoverageSourcePrinter.UNDEFINED)
46+
.condAttr(isPainted, "data-html-tooltip", isPainted ? getTooltip(line) : StringUtils.EMPTY)
47+
.with(
48+
td().withClass("line")
49+
.with(a().withName(String.valueOf(line)).withText(String.valueOf(line))),
50+
td().withClass("hits")
51+
.with(isPainted ? text(getSummaryColumn(line)) : text(StringUtils.EMPTY)),
52+
td().withClass("code")
53+
.with(rawHtml(SANITIZER.render(cleanupCode(sourceCode)))))
54+
.render();
55+
}
56+
57+
private String cleanupCode(final String content) {
58+
return content.replace("\n", StringUtils.EMPTY)
59+
.replace("\r", StringUtils.EMPTY)
60+
.replace(" ", NBSP)
61+
.replace("\t", NBSP.repeat(8));
62+
}
63+
64+
final int size() {
65+
return linesToPaint.length;
66+
}
67+
68+
public String getColorClass(final int line) {
69+
if (getCovered(line) == 0) {
70+
return NO_COVERAGE;
71+
}
72+
else if (getMissed(line) == 0) {
73+
return FULL_COVERAGE;
74+
}
75+
else {
76+
return PARTIAL_COVERAGE;
77+
}
78+
}
79+
80+
public String getTooltip(final int line) {
81+
var covered = getCovered(line);
82+
var missed = getMissed(line);
83+
if (covered + missed > 1) {
84+
if (missed == 0) {
85+
return "All branches covered";
86+
}
87+
return String.format("Partially covered, branch coverage: %d/%d", covered, covered + missed);
88+
}
89+
else if (covered == 1) {
90+
return "Covered at least once";
91+
}
92+
else {
93+
return "Not covered";
94+
}
95+
}
96+
97+
public String getSummaryColumn(final int line) {
98+
var covered = getCovered(line);
99+
var missed = getMissed(line);
100+
if (covered + missed > 1) {
101+
return String.format("%d/%d", covered, covered + missed);
102+
}
103+
return String.valueOf(covered);
104+
}
105+
106+
public final String getPath() {
107+
return path;
108+
}
109+
110+
public boolean isPainted(final int line) {
111+
return findIndexOfLine(line) >= 0;
112+
}
113+
114+
int findIndexOfLine(final int line) {
115+
return Arrays.binarySearch(linesToPaint, line);
116+
}
117+
118+
public int getCovered(final int line) {
119+
return getCounter(line, coveredPerLine);
120+
}
121+
122+
public int getMissed(final int line) {
123+
return getCounter(line, missedPerLine);
124+
}
125+
126+
int getCounter(final int line, final int... counters) {
127+
var index = findIndexOfLine(line);
128+
if (index >= 0) {
129+
return counters[index];
130+
}
131+
return 0;
132+
}
133+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package io.jenkins.plugins.coverage.metrics.source;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
import java.util.Map.Entry;
6+
import java.util.NavigableMap;
7+
import java.util.function.Predicate;
8+
import java.util.stream.Collectors;
9+
10+
import org.apache.commons.lang3.StringUtils;
11+
12+
import edu.hm.hafner.coverage.FileNode;
13+
import edu.hm.hafner.coverage.Mutation;
14+
15+
import j2html.tags.ContainerTag;
16+
import j2html.tags.UnescapedText;
17+
18+
import static j2html.TagCreator.*;
19+
20+
/**
21+
* Provides all required information for a {@link FileNode} so that its source code can be rendered together with the
22+
* line and mutation coverage in HTML.
23+
*/
24+
class MutationSourcePrinter extends CoverageSourcePrinter {
25+
private static final long serialVersionUID = -2215657894423024907L;
26+
27+
private final int[] survivedPerLine;
28+
private final int[] killedPerLine;
29+
private final String[] tooltipPerLine;
30+
31+
MutationSourcePrinter(final FileNode file) {
32+
super(file);
33+
34+
survivedPerLine = new int[size()];
35+
killedPerLine = new int[size()];
36+
tooltipPerLine = new String[size()];
37+
Arrays.fill(tooltipPerLine, StringUtils.EMPTY);
38+
39+
extractMutationDetails(file.getMutationsPerLine());
40+
41+
for (Mutation mutation : file.getMutations()) {
42+
if (mutation.hasSurvived()) {
43+
survivedPerLine[findIndexOfLine(mutation.getLine())]++;
44+
}
45+
else if (mutation.isKilled()) {
46+
killedPerLine[findIndexOfLine(mutation.getLine())]++;
47+
}
48+
}
49+
}
50+
51+
private void extractMutationDetails(final NavigableMap<Integer, List<Mutation>> mutationsPerLine) {
52+
for (Entry<Integer, List<Mutation>> entry : mutationsPerLine.entrySet()) {
53+
var indexOfLine = findIndexOfLine(entry.getKey());
54+
55+
tooltipPerLine[indexOfLine] = createInfo(entry.getValue());
56+
}
57+
}
58+
59+
private String createInfo(final List<Mutation> allMutations) {
60+
ContainerTag killedContainer = listMutations(allMutations,
61+
Mutation::isKilled, "Killed Mutations:");
62+
ContainerTag survivedContainer = listMutations(allMutations,
63+
Mutation::hasSurvived, "Survived Mutations:");
64+
if (killedContainer.getNumChildren() == 0 && survivedContainer.getNumChildren() == 0) {
65+
return "Not covered";
66+
}
67+
return div().with(killedContainer, survivedContainer).render();
68+
}
69+
70+
private ContainerTag listMutations(final List<Mutation> allMutations,
71+
final Predicate<Mutation> predicate, final String title) {
72+
var filtered = div();
73+
var killed = asBulletPoints(allMutations, predicate);
74+
if (!killed.isEmpty()) {
75+
filtered.with(div().with(new UnescapedText(title), ul().with(killed)));
76+
}
77+
return filtered;
78+
}
79+
80+
private List<ContainerTag> asBulletPoints(final List<Mutation> mutations, final Predicate<Mutation> predicate) {
81+
return mutations.stream().filter(predicate).map(mutation ->
82+
li().withText(String.format("%s (%s)", mutation.getDescription(), mutation.getMutator())))
83+
.collect(Collectors.toList());
84+
}
85+
86+
public int getSurvived(final int line) {
87+
return getCounter(line, survivedPerLine);
88+
}
89+
90+
public int getKilled(final int line) {
91+
return getCounter(line, killedPerLine);
92+
}
93+
94+
@Override
95+
public String getColorClass(final int line) {
96+
if (getCovered(line) == 0) {
97+
return NO_COVERAGE;
98+
}
99+
if (getKilled(line) == 0) {
100+
return NO_COVERAGE;
101+
}
102+
else if (getSurvived(line) == 0) {
103+
return FULL_COVERAGE;
104+
}
105+
else {
106+
return PARTIAL_COVERAGE;
107+
}
108+
}
109+
110+
@Override
111+
public String getTooltip(final int line) {
112+
return StringUtils.defaultIfBlank(tooltipPerLine[findIndexOfLine(line)], super.getTooltip(line));
113+
}
114+
115+
@Override
116+
public String getSummaryColumn(final int line) {
117+
var killed = getKilled(line);
118+
var survived = getSurvived(line);
119+
if (survived + killed > 0) {
120+
return String.format("%d/%d", killed, survived + killed);
121+
}
122+
return String.valueOf(killed);
123+
}
124+
}

0 commit comments

Comments
 (0)