org.conqat.engine.sourcecode.coverage.LineCoverageInfo Maven / Gradle / Ivy
Show all versions of teamscale-commons Show documentation
/*
* 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.checkerframework.checker.nullness.qual.Nullable;
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.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Holds line coverage information for a file.
*/
@IndexValueClass(containedInBackup = true)
public class LineCoverageInfo implements Serializable {
/** Value signaling an undefined timestamp */
public static final long TIMESTAMP_UNDEFINED = -1;
/** Version for serialization. */
private static final long serialVersionUID = 1L;
/** 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 {@link #TIMESTAMP_UNDEFINED} if
* unknown (which should be the exception since there shall always be a commit that added the
* coverage).
*
* Value dropped for serialization---see {@link #copyWithoutCommitTimestamps()}.
*/
@JsonIgnore
private long uploadCommitTimestamp;
/**
* The timestamp of the code commit this coverage data refers to, {@link #TIMESTAMP_UNDEFINED} if
* unknown. We only merge coverage infos that refer to the same code commit ({@link #merge}. This is
* primarily relevant for our upload pipeline that creates additional (artificial) upload commits
* (with new timestamps), with each upload.
*
* If {@link #TIMESTAMP_UNDEFINED}, then {@link #uploadCommitTimestamp} is used to decide on
* merging, which is relevant for the S3/Artifact store pipeline. Setting it to the same value as in
* {@link #uploadCommitTimestamp} will lead to the same merge decision---see {@link #merge}.
*
* Value dropped for serialization---see {@link #copyWithoutCommitTimestamps()}.
*/
@JsonIgnore
private long codeCommitTimestamp;
/**
* The branch name the coverage has been uploaded for (written to the index) last.
*
* The value might be {@code null} in case the entity came from an old backup. In that case, we do
* not merge newly uploaded coverage into the present {@link LineCoverageInfo} object.
*/
@Nullable
@JsonIgnore
private String branchName;
public LineCoverageInfo(LineCoverageInfo lineCoverage) {
uploadCommitTimestamp = lineCoverage.uploadCommitTimestamp;
codeCommitTimestamp = lineCoverage.codeCommitTimestamp;
branchName = lineCoverage.branchName;
isMethodAccurate = lineCoverage.isMethodAccurate;
fullyCoveredLines.addAll(lineCoverage.fullyCoveredLines);
partiallyCoveredLines.addAll(lineCoverage.partiallyCoveredLines);
uncoveredLines.addAll(lineCoverage.uncoveredLines);
}
/** Constructor with a default timestamp. */
@JsonCreator
public LineCoverageInfo(@JsonProperty(IS_METHOD_ACCURATE_PROPERTY) boolean isMethodAccurate) {
this(TIMESTAMP_UNDEFINED, TIMESTAMP_UNDEFINED, isMethodAccurate);
}
/** Constructor */
public LineCoverageInfo(long uploadCommitTimestamp, long codeCommitTimestamp, boolean isMethodAccurate) {
this.uploadCommitTimestamp = uploadCommitTimestamp;
this.codeCommitTimestamp = codeCommitTimestamp;
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 #uploadCommitTimestamp
*/
public long getUploadCommitTimestamp() {
return uploadCommitTimestamp;
}
/**
* @see #uploadCommitTimestamp
*/
public void setUploadCommitTimestamp(long uploadCommitTimestamp) {
this.uploadCommitTimestamp = uploadCommitTimestamp;
}
/**
* @see #codeCommitTimestamp
*/
public void setCodeCommitTimestamp(long codeCommitTimestamp) {
this.codeCommitTimestamp = codeCommitTimestamp;
}
/**
* @see #codeCommitTimestamp
*/
public long getCodeCommitTimestamp() {
return codeCommitTimestamp;
}
/**
* @see #branchName
*/
public void setBranchName(String branchName) {
this.branchName = branchName;
}
/** {@inheritDoc} */
@Override
public String toString() {
return String.valueOf(getCoverageRatio());
}
/**
* Returns a string representation of the covered/uncovered lines.
*
* ATTENTION: Changing this method can break many tests, which rely on the formatting produced by
* it.
*/
public String toLineString() {
String result = "Fully covered: " + fullyCoveredLines + "; partially covered: " + partiallyCoveredLines
+ "; uncovered: " + uncoveredLines;
if (uploadCommitTimestamp != codeCommitTimestamp && codeCommitTimestamp != 0) {
// `0` value in `codeCommitTimestamp` might be because it is ignored by the JSON
// serialization, can happen in test code.
result = result + "; upload timestamp: " + uploadCommitTimestamp + "; code timestamp: "
+ codeCommitTimestamp;
}
return result;
}
/**
* 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.
*
* Some background information: The timestamp information can be re-constructed from the other
* indexes, especially because the data is stored in a historized fashion.
*
* Line coverage information tends to be rather stable over time, that is, there is a huge potential
* for compression. If we store the timestamp with the coverage info object, we would hinder this
* (delta) compression.
*/
public LineCoverageInfo copyWithoutCommitTimestamps() {
LineCoverageInfo copy = new LineCoverageInfo(this);
copy.setUploadCommitTimestamp(TIMESTAMP_UNDEFINED);
copy.setCodeCommitTimestamp(TIMESTAMP_UNDEFINED);
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;
}
/**
* @return a new {@link LineCoverageInfo} by merging the two given ones, or by returning the newer
* one in case the timestamps are different.
*
* @param existingInfo
* existing (older) coverage infos that might already aggregate other (even older) infos.
* @param infoToAdd
* coverage infos to add (new info from a report).
*/
public static LineCoverageInfo merge(LineCoverageInfo existingInfo, @Nullable LineCoverageInfo infoToAdd) {
if (infoToAdd == null) {
return existingInfo;
} else if (existingInfo.branchName == null) {
// In case the branch name is not yet present on the existing info object, it
// stems from an old backup. We do not merge on that.
return infoToAdd;
}
CCSMAssert.isTrue(infoToAdd.branchName != null, "The branch name of the info to add must be set.");
if (!infoToAdd.branchName.equals(existingInfo.branchName)) {
return infoToAdd;
}
CCSMAssert.isTrue(existingInfo.codeCommitTimestamp <= infoToAdd.codeCommitTimestamp,
"`infoToAdd` must be more recent that those already processed (`existingInfo`).");
// We only merge with the existing data if the same timestamp is used.
if (existingInfo.codeCommitTimestamp == TIMESTAMP_UNDEFINED) {
// If now code commit is defined, the upload timestamps must be equal for doing
// a merge of the coverage; typical for the S3 pipeline.
if (existingInfo.uploadCommitTimestamp != infoToAdd.uploadCommitTimestamp) {
return infoToAdd;
}
} else if (existingInfo.codeCommitTimestamp != infoToAdd.codeCommitTimestamp) {
// If the old data is for a different code timestamp, we yield the new coverage.
return infoToAdd;
}
LineCoverageInfo result = new LineCoverageInfo(existingInfo);
// Keep track of the most recent commit that added data. Used later for some
// optimizations. Data build from the current report needs the current
// timestamp.
result.setUploadCommitTimestamp(Math.max(existingInfo.uploadCommitTimestamp, infoToAdd.uploadCommitTimestamp));
// Merging a method accurate info with a line accurate one will make it less
// accurate, that is method accurate.
result.setMethodAccurate(existingInfo.isMethodAccurate || infoToAdd.isMethodAccurate);
// Merge the actual line coverage:
// - If fully covered in one, make it fully covered in result.
result.addLineCoverage(infoToAdd.fullyCoveredLines, ELineCoverage.FULLY_COVERED);
// - Partially covered in one, make it partially covered in result.
result.addLineCoverage(infoToAdd.partiallyCoveredLines, ELineCoverage.PARTIALLY_COVERED);
result.partiallyCoveredLines.removeAll(result.fullyCoveredLines);
// - If coverable in one, then coverable in result.
result.addLineCoverage(infoToAdd.uncoveredLines, ELineCoverage.NOT_COVERED);
result.uncoveredLines.removeAll(result.fullyCoveredLines);
result.uncoveredLines.removeAll(result.partiallyCoveredLines);
return result;
}
/**
* We do not serialize the timestamp values of {@link LineCoverageInfo} and set them to an
* 'undefined' value in {@link #copyWithoutCommitTimestamps}.
*
* We expect {@link #TIMESTAMP_UNDEFINED} to indicate an undefined timestamp.
*/
public static boolean isUndefinedTimestamp(long timestamp) {
return timestamp == TIMESTAMP_UNDEFINED;
}
}