org.conqat.engine.sourcecode.coverage.LineCoverageInfo Maven / Gradle / Ivy
/*
* Copyright (c) CQSE GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.conqat.engine.sourcecode.coverage;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.collections.CompactLines;
import org.conqat.lib.commons.region.LineBasedRegion;
import org.conqat.lib.commons.test.IndexValueClass;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Holds line coverage information for a file.
*/
@IndexValueClass(containedInBackup = true)
public class LineCoverageInfo implements Serializable {
/** Version for serialization. */
private static final long serialVersionUID = 1L;
/** The name of the JSON property name for {@link #timestamp}. */
private static final String TIMESTAMP_PROPERTY = "timestamp";
/** The name of the JSON property name for {@link #isMethodAccurate}. */
private static final String IS_METHOD_ACCURATE_PROPERTY = "isMethodAccurate";
/** The line numbers that were fully covered */
@JsonProperty("fullyCoveredLines")
@JsonAlias("fully-covered-lines")
private final CompactLines fullyCoveredLines = new CompactLines();
/** The line numbers that were partially covered */
@JsonProperty("partiallyCoveredLines")
@JsonAlias("partially-covered-lines")
private final CompactLines partiallyCoveredLines = new CompactLines();
/** The line numbers that were not covered */
@JsonProperty("uncoveredLines")
@JsonAlias("uncovered-lines")
private final CompactLines uncoveredLines = new CompactLines();
/**
* Determines the accuracy of this coverage info. If false
, the
* info is line-accurate, otherwise it is only method-accurate (i.e. executing
* any statement in the method will mark the entire method as executed). The
* default is line-accurate.
*
* Note that method-accurate coverage should not be used when e.g. calculating
* the line coverage metric.
*/
@JsonProperty(IS_METHOD_ACCURATE_PROPERTY)
private boolean isMethodAccurate;
/**
* The timestamp of the code this coverage data refers to. May be -1 if unknown.
*/
@JsonProperty(TIMESTAMP_PROPERTY)
private long timestamp;
public LineCoverageInfo(LineCoverageInfo lineCoverage) {
timestamp = lineCoverage.timestamp;
isMethodAccurate = lineCoverage.isMethodAccurate;
fullyCoveredLines.addAll(lineCoverage.fullyCoveredLines);
partiallyCoveredLines.addAll(lineCoverage.partiallyCoveredLines);
uncoveredLines.addAll(lineCoverage.uncoveredLines);
}
/** Constructor with a default timestamp. */
public LineCoverageInfo(boolean isMethodAccurate) {
this(-1, isMethodAccurate);
}
/** Constructor */
@JsonCreator
public LineCoverageInfo(@JsonProperty(TIMESTAMP_PROPERTY) long timestamp,
@JsonProperty(IS_METHOD_ACCURATE_PROPERTY) boolean isMethodAccurate) {
this.timestamp = timestamp;
this.isMethodAccurate = isMethodAccurate;
}
/** @see #isMethodAccurate */
public boolean isMethodAccurate() {
return isMethodAccurate;
}
/** @see #isMethodAccurate */
public void setMethodAccurate(boolean isMethodAccurate) {
this.isMethodAccurate = isMethodAccurate;
}
/**
* Adds the coverage information for the given line. This merges the given
* coverage info if called multiple times for the same line. This is needed to
* allow for overlapping coverage reports.
*/
public void addLineCoverage(int line, ELineCoverage coverage) {
CCSMAssert.isNotNull(coverage);
if (fullyCoveredLines.contains(line)) {
// cannot get any better
} else if (partiallyCoveredLines.contains(line)) {
if (coverage == ELineCoverage.FULLY_COVERED) {
partiallyCoveredLines.remove(line);
fullyCoveredLines.add(line);
}
} else {
uncoveredLines.remove(line);
setLineCoverage(line, coverage);
}
}
/**
* Adds the coverage information for the given lines.
*
* @see #addLineCoverage(CompactLines, ELineCoverage)
*/
public void addLineCoverage(Collection lines, ELineCoverage coverage) {
addLineCoverage(new CompactLines(lines), coverage);
}
/**
* Adds the coverage information for the given lines. This preserves the
* "highest" coverage value i.e., adding {@link ELineCoverage#NOT_COVERED} or
* {@link ELineCoverage#PARTIALLY_COVERED} to a line that is already
* {@link ELineCoverage#FULLY_COVERED} this is a NOP.
*/
public void addLineCoverage(CompactLines lines, ELineCoverage coverage) {
if (Objects.requireNonNull(coverage) == ELineCoverage.FULLY_COVERED) {
fullyCoveredLines.addAll(lines);
partiallyCoveredLines.removeAll(lines);
uncoveredLines.removeAll(lines);
} else if (coverage == ELineCoverage.PARTIALLY_COVERED) {
partiallyCoveredLines.addAll(lines);
partiallyCoveredLines.removeAll(fullyCoveredLines);
uncoveredLines.removeAll(lines);
} else {
uncoveredLines.addAll(lines);
uncoveredLines.removeAll(fullyCoveredLines);
uncoveredLines.removeAll(partiallyCoveredLines);
}
}
/** Adds coverage information from startLine to endLine */
public void addRangeCoverage(int startLine, int endLine, ELineCoverage coverage) {
for (int i = startLine; i <= endLine; ++i) {
addLineCoverage(i, coverage);
}
}
/**
* Adds the coverage information for the given lines.
*
* @see #addLineCoverage(int, ELineCoverage)
*/
public void addLineCoverage(IntStream lines, ELineCoverage coverage) {
lines.forEach(line -> addLineCoverage(line, coverage));
}
/** Removes all previously stored line coverage for the given line. */
public void removeLineCoverageInfo(int line) {
fullyCoveredLines.remove(line);
partiallyCoveredLines.remove(line);
uncoveredLines.remove(line);
}
/**
* Sets the line coverage for the given line. This ignores previously stored
* values.
*/
private void setLineCoverage(int line, ELineCoverage coverage) {
switch (coverage) {
case FULLY_COVERED:
fullyCoveredLines.add(line);
break;
case PARTIALLY_COVERED:
partiallyCoveredLines.add(line);
break;
case NOT_COVERED:
uncoveredLines.add(line);
break;
default:
throw new IllegalStateException("Unknown line coverage: " + coverage);
}
}
/** Adds all coverage information from another {@link LineCoverageInfo}. */
public void addAll(LineCoverageInfo coverageInfo) {
addLineCoverage(coverageInfo.fullyCoveredLines, ELineCoverage.FULLY_COVERED);
addLineCoverage(coverageInfo.partiallyCoveredLines, ELineCoverage.PARTIALLY_COVERED);
addLineCoverage(coverageInfo.uncoveredLines, ELineCoverage.NOT_COVERED);
}
/**
* Returns the line coverage for the given line or null
if none is
* stored.
*/
public ELineCoverage getLineCoverage(int line) {
if (fullyCoveredLines.contains(line)) {
return ELineCoverage.FULLY_COVERED;
}
if (partiallyCoveredLines.contains(line)) {
return ELineCoverage.PARTIALLY_COVERED;
}
if (uncoveredLines.contains(line)) {
return ELineCoverage.NOT_COVERED;
}
return null;
}
/** Returns list of fully covered lines (sorted ascending) */
public CompactLines getFullyCoveredLines() {
return fullyCoveredLines;
}
/** Returns list of partially covered lines (sorted ascending) */
public CompactLines getPartiallyCoveredLines() {
return partiallyCoveredLines;
}
/** Returns list of uncovered lines (sorted ascending) */
public CompactLines getUncoveredLines() {
return uncoveredLines;
}
/**
* Returns list of lines that are either fully or partially covered (sorted
* ascending)
*/
public CompactLines getCoveredLines() {
CompactLines coveredLines = new CompactLines();
coveredLines.addAll(partiallyCoveredLines);
coveredLines.addAll(fullyCoveredLines);
return coveredLines;
}
/** Returns the line coverage ratio as a double ([0..1]). */
private double getCoverageRatio() {
int lines = getCoverableLines();
if (lines == 0) {
return 0;
}
return getCoveredLineCount() / lines;
}
/** Returns the number of lines that are covered or partially covered. */
public double getCoveredLineCount() {
return fullyCoveredLines.size() + partiallyCoveredLines.size();
}
/** Returns the number of lines that are coverable. */
public int getCoverableLines() {
return fullyCoveredLines.size() + partiallyCoveredLines.size() + uncoveredLines.size();
}
/** Returns the set of all lines in the coverage report. */
public CompactLines getAllCoverableLines() {
CompactLines coverableLines = new CompactLines();
coverableLines.addAll(fullyCoveredLines);
coverableLines.addAll(partiallyCoveredLines);
coverableLines.addAll(uncoveredLines);
return coverableLines;
}
/**
* Get the highest line number of any coverable line in this coverage info.
* Returns 0 if there are no coverable lines.
*/
public int getHighestLineNumber() {
return getAllCoverableLines().getHighestLineNumber().orElse(0);
}
/**
* @see #timestamp
*/
public long getTimestamp() {
return timestamp;
}
/**
* @see #timestamp
*/
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
/** {@inheritDoc} */
@Override
public String toString() {
return String.valueOf(getCoverageRatio());
}
/** Returns a string representation of the covered/uncovered lines. */
public String toLineString() {
return "Fully covered: " + fullyCoveredLines + "; partially covered: " + partiallyCoveredLines + "; uncovered: "
+ uncoveredLines + "; timestamp: " + timestamp;
}
/**
* Clears {@link #uncoveredLines}, {@link #partiallyCoveredLines} and
* {@link #fullyCoveredLines}.
*/
public void clear() {
fullyCoveredLines.clear();
partiallyCoveredLines.clear();
uncoveredLines.clear();
}
/**
* Replaces the coverable lines with the given lines. This also adjusts the
* {@link #fullyCoveredLines} and {@link #partiallyCoveredLines} by removing all
* lines that are not coverable.
*/
public void setCoverableLines(CompactLines lines) {
fullyCoveredLines.retainAll(lines);
partiallyCoveredLines.retainAll(lines);
uncoveredLines.clear();
uncoveredLines.addAll(lines);
uncoveredLines.removeAll(fullyCoveredLines);
uncoveredLines.removeAll(partiallyCoveredLines);
}
/**
* Creates a copy of this object that is stable in regard to serialization by
* omitting the timestamp value from the copy.
*/
public LineCoverageInfo copyWithoutTimestamp() {
LineCoverageInfo copy = new LineCoverageInfo(this);
copy.setTimestamp(-1);
return copy;
}
/**
* Extends coverage to full entities by using the best covered line for all
* lines of an entity.
*/
public void extendCoverageToStatements(List multiLineRegions) {
for (LineBasedRegion region : multiLineRegions) {
int startLine = region.getStart();
int endLine = region.getEnd();
if (IntStream.range(startLine, endLine + 1).anyMatch(fullyCoveredLines::contains)) {
IntStream.range(startLine, endLine + 1).forEach(fullyCoveredLines::add);
IntStream.range(startLine, endLine + 1).forEach(partiallyCoveredLines::remove);
IntStream.range(startLine, endLine + 1).forEach(uncoveredLines::remove);
} else if (IntStream.range(startLine, endLine + 1).anyMatch(partiallyCoveredLines::contains)) {
IntStream.range(startLine, endLine + 1).forEach(partiallyCoveredLines::add);
IntStream.range(startLine, endLine + 1).forEach(uncoveredLines::remove);
}
}
}
/**
* Returns the estimated size of the object in bytes. This is used to determine
* how much we can keep in the in-memory cache at once.
*/
public long getEstimatedSizeBytes() {
// We estimate 32bytes per HashNode including the Integer and 60bytes for the
// HashSets and timestamp in LineCoverageInfo
return 60L + (fullyCoveredLines.size() + partiallyCoveredLines.size() + uncoveredLines.size()) * 32L;
}
}