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
Show all changes
21 commits
Select commit Hold shift + click to select a range
50ed298
Improve XML serialization memory footprint.
uhafner Apr 23, 2022
77ee6ba
Fix markdown checker.
uhafner Apr 23, 2022
900bfa5
Introduce a builder for `Coverage` instances.
uhafner Apr 23, 2022
148e8a2
Implement flyweight pattern for coverage instances.
uhafner Apr 23, 2022
42cc779
Fix initialization order of cached values.
uhafner Apr 23, 2022
00fb157
Add another test to check the boundaries of the cache.
uhafner Apr 24, 2022
2e6a0ab
Ignore PMD warning, there is no better option to handle this decision.
uhafner Apr 24, 2022
f1e26b0
Merge remote-tracking branch 'fo-code/change-coverage' into coverage-…
uhafner May 6, 2022
9a5db75
Add serialization for `CoveragePercentage`.
uhafner May 6, 2022
66bae6e
Merge remote-tracking branch 'origin/master' into coverage-serialization
uhafner May 14, 2022
27e1a54
Fix coverage converters.
uhafner May 14, 2022
51067e6
Remove unused variable.
uhafner May 15, 2022
7697f7d
Fix broken serialization if numerator is greater than denominator.
uhafner May 16, 2022
b8425c4
Use `valueOf` without prefix.
uhafner May 16, 2022
993c828
Add a converter for the coverage per line mappings.
uhafner May 18, 2022
6367d6f
Add a converter for the percentage per metric mappings.
uhafner May 19, 2022
1fd0db3
Move XStream code to toplevel class `CoverageXmlStream`.
uhafner May 19, 2022
59232cb
Create base class for `Map` based converters.
uhafner May 19, 2022
c2fb9a8
Add a converter for all mappings in `FileCoverageNode`.
uhafner May 19, 2022
1f3f6ac
Remove unused imports.
uhafner May 20, 2022
b9ef6ef
Fix warnings.
uhafner May 20, 2022
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
170 changes: 163 additions & 7 deletions plugin/src/main/java/io/jenkins/plugins/coverage/model/Coverage.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import java.io.Serializable;
import java.util.Locale;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.Fraction;

import edu.hm.hafner.util.VisibleForTesting;

/**
* Value of a code coverage item. The code coverage is measured using the number of covered and missed items. The type
* of items (line, instruction, branch, file, etc.) is provided by the companion class {@link CoverageMetric}.
Expand All @@ -17,6 +20,39 @@ public final class Coverage implements Serializable {
/** Null object that indicates that the code coverage has not been measured. */
public static final Coverage NO_COVERAGE = new Coverage(0, 0);

/**
* Creates a new {@link Coverage} instance from the provided string representation. The string representation is
* expected to contain the number of covered items and the total number of items - separated by a slash, e.g.
* "100/345", or "0/0". Whitespace characters will be ignored.
*
* @param stringRepresentation
* string representation to convert from
*
* @return the created coverage
* @throws IllegalArgumentException
* if the string is not a valid Coverage instance
*/
public static Coverage valueOf(final String stringRepresentation) {
try {
String cleanedFormat = StringUtils.deleteWhitespace(stringRepresentation);
if (StringUtils.contains(cleanedFormat, "/")) {
String extractedCovered = StringUtils.substringBefore(cleanedFormat, "/");
String extractedTotal = StringUtils.substringAfter(cleanedFormat, "/");

int covered = Integer.parseInt(extractedCovered);
int total = Integer.parseInt(extractedTotal);
if (total >= covered) {
return new CoverageBuilder().setCovered(covered).setMissed(total - covered).build();
}
}
}
catch (NumberFormatException exception) {
// ignore and throw a specific exception
}
throw new IllegalArgumentException(
String.format("Cannot convert %s to a valid Coverage instance.", stringRepresentation));
}

private final int covered;
private final int missed;

Expand All @@ -28,7 +64,7 @@ public final class Coverage implements Serializable {
* @param missed
* the number of missed items
*/
public Coverage(final int covered, final int missed) {
private Coverage(final int covered, final int missed) {
this.covered = covered;
this.missed = missed;
}
Expand Down Expand Up @@ -80,8 +116,8 @@ public int getRoundedPercentage() {
}

/**
* Formats the covered percentage as String (with a precision of two digits after the comma). Uses {@code
* Locale.getDefault()} to format the percentage.
* Formats the covered percentage as String (with a precision of two digits after the comma). Uses
* {@code Locale.getDefault()} to format the percentage.
*
* @return the covered percentage
* @see #formatCoveredPercentage(Locale)
Expand Down Expand Up @@ -136,8 +172,8 @@ public CoveragePercentage getMissedPercentage() {
}

/**
* Formats the missed percentage as formatted String (with a precision of two digits after the comma). Uses {@code
* Locale.getDefault()} to format the percentage.
* Formats the missed percentage as formatted String (with a precision of two digits after the comma). Uses
* {@code Locale.getDefault()} to format the percentage.
*
* @return the missed percentage
*/
Expand Down Expand Up @@ -173,8 +209,9 @@ private String printPercentage(final Locale locale, final CoveragePercentage cov
* @return the sum of this and the additional coverage
*/
public Coverage add(final Coverage additional) {
return new Coverage(covered + additional.getCovered(),
missed + additional.getMissed());
return new CoverageBuilder().setCovered(covered + additional.getCovered())
.setMissed(missed + additional.getMissed())
.build();
}

@Override
Expand Down Expand Up @@ -217,4 +254,123 @@ public int hashCode() {
result = 31 * result + missed;
return result;
}

/**
* Returns a string representation for this {@link Coverage} that can be used to serialize this instance in a simple
* but still readable way. The serialization contains the number of covered items and the total number of items -
* separated by a slash, e.g. "100/345", or "0/0".
*
* @return a string representation for this {@link Coverage}
*/
public String serializeToString() {
return String.format("%d/%d", getCovered(), getTotal());
}

/**
* Builder to create an cache new {@link Coverage} instances.
*/
public static class CoverageBuilder {
@VisibleForTesting
static final int CACHE_SIZE = 16;
private static final Coverage[] CACHE = new Coverage[CACHE_SIZE * CACHE_SIZE];

static {
for (int covered = 0; covered < CACHE_SIZE; covered++) {
for (int missed = 0; missed < CACHE_SIZE; missed++) {
CACHE[getCacheIndex(covered, missed)] = new Coverage(covered, missed);
}
}
}

private static int getCacheIndex(final int covered, final int missed) {
return covered * CACHE_SIZE + missed;
}

/** Null object that indicates that the code coverage has not been measured. */
public static final Coverage NO_COVERAGE = CACHE[0];

private int covered;
private boolean isCoveredSet;
private int missed;
private boolean isMissedSet;
private int total;
private boolean isTotalSet;

/**
* Sets the number of total items.
*
* @param total
* the number of total items
*
* @return this
*/
public CoverageBuilder setTotal(final int total) {
this.total = total;
isTotalSet = true;
return this;
}

/**
* Sets the number of covered items.
*
* @param covered
* the number of covered items
*
* @return this
*/
public CoverageBuilder setCovered(final int covered) {
this.covered = covered;
isCoveredSet = true;
return this;
}

/**
* Sets the number of missed items.
*
* @param missed
* the number of missed items
*
* @return this
*/
public CoverageBuilder setMissed(final int missed) {
this.missed = missed;
isMissedSet = true;
return this;
}

/**
* Creates the new {@link Coverage} instance.
*
* @return the new instance
*/
@SuppressWarnings("PMD.CyclomaticComplexity")
public Coverage build() {
if (isCoveredSet && isMissedSet && isTotalSet) {
throw new IllegalArgumentException(
"Setting all three values covered, missed, and total is not allowed, just select two of them.");
}
if (isTotalSet) {
if (isCoveredSet) {
return createOrGetCoverage(covered, total - covered);
}
else if (isMissedSet) {
return createOrGetCoverage(total - missed, missed);
}
}
else {
if (isCoveredSet && isMissedSet) {
return createOrGetCoverage(covered, missed);
}
}
throw new IllegalArgumentException("You must set exactly two properties.");
}

@SuppressWarnings({"checkstyle:HiddenField", "ParameterHidesMemberVariable"})
private Coverage createOrGetCoverage(final int covered, final int missed) {
if (covered < CACHE_SIZE && missed < CACHE_SIZE) {
return CACHE[getCacheIndex(covered, missed)];
}
return new Coverage(covered, missed);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@
import java.util.SortedMap;
import java.util.TreeMap;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
Expand All @@ -24,7 +18,6 @@
import hudson.model.HealthReport;
import hudson.model.HealthReportingAction;
import hudson.model.Run;
import hudson.util.XStream2;

import io.jenkins.plugins.forensics.reference.ReferenceBuild;
import io.jenkins.plugins.util.AbstractXmlStream;
Expand Down Expand Up @@ -527,55 +520,4 @@ public String getUrlName() {
return DETAILS_URL;
}

/**
* {@link Converter} for {@link CoverageMetric} instances so that only the string name will be serialized. After
* reading the values back from the stream, the string representation will be converted to an actual instance
* again.
*/
private static final class MetricsConverter implements Converter {
@SuppressWarnings("PMD.NullAssignment")
@Override
public void marshal(final Object source, final HierarchicalStreamWriter writer,
final MarshallingContext context) {
writer.setValue(source instanceof CoverageMetric ? ((CoverageMetric) source).getName() : null);
}

@Override
public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
return CoverageMetric.valueOf(reader.getValue());
}

@Override
public boolean canConvert(final Class type) {
return type == CoverageMetric.class;
}
}

/**
* Configures the XML stream for the coverage tree, which consists of {@link CoverageNode}.
*/
static class CoverageXmlStream extends AbstractXmlStream<CoverageNode> {

/**
* Creates a XML stream for {@link CoverageNode}.
*/
CoverageXmlStream() {
super(CoverageNode.class);
}

@Override
protected void configureXStream(final XStream2 xStream) {
xStream.alias("node", CoverageNode.class);
xStream.alias("leaf", CoverageLeaf.class);
xStream.alias("coverage", Coverage.class);
xStream.alias("coveragePercentage", CoveragePercentage.class);
xStream.addImmutableType(CoverageMetric.class, false);
xStream.registerConverter(new MetricsConverter());
}

@Override
protected CoverageNode createDefaultValue() {
return new CoverageNode(CoverageMetric.MODULE, "Empty");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.io.Serializable;
import java.util.Objects;

import io.jenkins.plugins.coverage.model.Coverage.CoverageBuilder;

/**
* A leaf in the coverage hierarchy. A leaf is a non-divisible coverage metric like line or branch coverage.
*
Expand Down Expand Up @@ -43,7 +45,7 @@ public Coverage getCoverage(final CoverageMetric searchMetric) {
if (metric.equals(searchMetric)) {
return coverage;
}
return Coverage.NO_COVERAGE;
return CoverageBuilder.NO_COVERAGE;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.CheckForNull;

import io.jenkins.plugins.coverage.model.Coverage.CoverageBuilder;

import one.util.streamex.StreamEx;

/**
Expand All @@ -39,8 +41,8 @@
public class CoverageNode implements Serializable {
private static final long serialVersionUID = -6608885640271135273L;

private static final Coverage COVERED_NODE = new Coverage(1, 0);
private static final Coverage MISSED_NODE = new Coverage(0, 1);
private static final Coverage COVERED_NODE = new Coverage.CoverageBuilder().setCovered(1).setMissed(0).build();
private static final Coverage MISSED_NODE = new Coverage.CoverageBuilder().setCovered(0).setMissed(1).build();

/** Transient non static {@link CoverageTreeCreator} in order to be able to mock it for tests. */
private transient CoverageTreeCreator coverageTreeCreator;
Expand Down Expand Up @@ -332,15 +334,15 @@ public Coverage getCoverage(final CoverageMetric searchMetric) {
if (searchMetric.isLeaf()) {
Coverage childrenCoverage = children.stream()
.map(node -> node.getCoverage(searchMetric))
.reduce(Coverage.NO_COVERAGE, Coverage::add);
.reduce(CoverageBuilder.NO_COVERAGE, Coverage::add);
return leaves.stream()
.map(node -> node.getCoverage(searchMetric))
.reduce(childrenCoverage, Coverage::add);
}
else {
Coverage childrenCoverage = children.stream()
.map(node -> node.getCoverage(searchMetric))
.reduce(Coverage.NO_COVERAGE, Coverage::add);
.reduce(CoverageBuilder.NO_COVERAGE, Coverage::add);

if (metric.equals(searchMetric)) {
if (getCoverage(CoverageMetric.LINE).getCovered() > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ private CoverageNode createNode(final CoverageResult result) {
for (Map.Entry<CoverageElement, Ratio> coverage : result.getLocalResults().entrySet()) {
Ratio ratio = coverage.getValue();
CoverageLeaf leaf = new CoverageLeaf(CoverageMetric.valueOf(coverage.getKey().getName()),
new Coverage((int) ratio.numerator, (int) (ratio.denominator - ratio.numerator)));
new Coverage.CoverageBuilder().setCovered((int) ratio.numerator)
.setMissed((int) (ratio.denominator - ratio.numerator))
.build());
coverageNode.add(leaf);
}
return coverageNode;
Expand Down Expand Up @@ -93,11 +95,14 @@ private void attachCoveragePerLine(final FileCoverageNode node, final CoveragePa
if (paint.getBranchTotal(line) > 0) {
int covered = paint.getBranchCoverage(line);
int missed = paint.getBranchTotal(line) - covered;
coverageDetails.put(line, new Coverage(paint.getBranchCoverage(line), missed));
coverageDetails.put(line, new Coverage.CoverageBuilder().setCovered(paint.getBranchCoverage(line))
.setMissed(missed)
.build());
}
else {
int covered = paint.getHits(line) > 0 ? 1 : 0;
coverageDetails.put(line, new Coverage(covered, 1 - covered));
coverageDetails.put(line,
new Coverage.CoverageBuilder().setCovered(covered).setMissed(1 - covered).build());
}
}
node.setCoveragePerLine(coverageDetails);
Expand Down
Loading