Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<testcontainers.version>1.18.0</testcontainers.version>
<job-dsl.version>1.83</job-dsl.version>

<coverage-model.version>0.23.0</coverage-model.version>
<coverage-model.version>0.24.0</coverage-model.version>
<git-forensics.version>2.0.0</git-forensics.version>
<prism-api.version>1.29.0-4</prism-api.version>
<pull-request-monitoring.version>1.7.8</pull-request-monitoring.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = "&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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Integer, List<Mutation>> mutationsPerLine) {
for (Entry<Integer, List<Mutation>> entry : mutationsPerLine.entrySet()) {
var indexOfLine = findIndexOfLine(entry.getKey());

tooltipPerLine[indexOfLine] = createInfo(entry.getValue());
}
}

private String createInfo(final List<Mutation> 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<Mutation> allMutations,
final Predicate<Mutation> 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<ContainerTag> asBulletPoints(final List<Mutation> mutations, final Predicate<Mutation> 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);
}
}
Loading