All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.engine.sourcecode.coverage.LineCoverageInfo Maven / Gradle / Ivy

The newest version!
/*
 * 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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy