001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 the ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.engine.sourcecode.coverage; 018 019import java.io.Serializable; 020import java.util.Collection; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Set; 024import java.util.stream.IntStream; 025 026import org.conqat.lib.commons.assertion.CCSMAssert; 027import org.conqat.lib.commons.collections.CollectionUtils; 028import org.conqat.lib.commons.js_export.ExportToJavaScript; 029import org.conqat.lib.commons.string.StringUtils; 030 031import com.fasterxml.jackson.annotation.JsonCreator; 032import com.fasterxml.jackson.annotation.JsonProperty; 033import com.thoughtworks.xstream.annotations.XStreamAlias; 034 035import eu.cqse.check.framework.scanner.ETokenType.ETokenClass; 036import eu.cqse.check.framework.scanner.IToken; 037import eu.cqse.check.framework.shallowparser.framework.ShallowEntity; 038 039/** 040 * Holds line coverage information for a file. 041 */ 042@ExportToJavaScript 043public class LineCoverageInfo implements Serializable { 044 045 /** Version for serialization. */ 046 private static final long serialVersionUID = 1L; 047 048 /** The name of the JSON property name for {@link #timestamp}. */ 049 private static final String TIMESTAMP_PROPERTY = "timestamp"; 050 051 /** The name of the JSON property name for {@link #isMethodAccurate}. */ 052 private static final String IS_METHOD_ACCURATE_PROPERTY = "isMethodAccurate"; 053 054 /** The line numbers that were fully covered */ 055 @JsonProperty("fullyCoveredLines") 056 @XStreamAlias("fully-covered-lines") 057 private final Set<Integer> fullyCoveredLines = new HashSet<>(); 058 059 /** The line numbers that were partially covered */ 060 @JsonProperty("partiallyCoveredLines") 061 @XStreamAlias("partially-covered-lines") 062 private final Set<Integer> partiallyCoveredLines = new HashSet<>(); 063 064 /** The line numbers that were not covered */ 065 @JsonProperty("uncoveredLines") 066 @XStreamAlias("uncovered-lines") 067 private final Set<Integer> uncoveredLines = new HashSet<>(); 068 069 /** 070 * Determines the accuracy of this coverage info. If <code>false</code>, the 071 * info is line-accurate, otherwise it is only method-accurate (i.e. executing 072 * any statement in the method will mark the entire method as executed). The 073 * default is line-accurate. 074 * 075 * Note that method-accurate coverage should not be used when e.g. calculating 076 * the line coverage metric. 077 */ 078 @JsonProperty(IS_METHOD_ACCURATE_PROPERTY) 079 private boolean isMethodAccurate = false; 080 081 /** 082 * @see #isMethodAccurate 083 */ 084 public boolean isMethodAccurate() { 085 return isMethodAccurate; 086 } 087 088 /** 089 * @see #isMethodAccurate 090 */ 091 public void setMethodAccurate(boolean isMethodAccurate) { 092 this.isMethodAccurate = isMethodAccurate; 093 } 094 095 /** 096 * The timestamp of the code this coverage data refers to. May be -1 if unknown. 097 */ 098 @JsonProperty(TIMESTAMP_PROPERTY) 099 private long timestamp; 100 101 /** Constructor with a default timestamp. */ 102 public LineCoverageInfo(boolean isMethodAccurate) { 103 this(-1, isMethodAccurate); 104 } 105 106 /** Constructor */ 107 @JsonCreator 108 public LineCoverageInfo(@JsonProperty(TIMESTAMP_PROPERTY) long timestamp, 109 @JsonProperty(IS_METHOD_ACCURATE_PROPERTY) boolean isMethodAccurate) { 110 this.timestamp = timestamp; 111 this.isMethodAccurate = isMethodAccurate; 112 } 113 114 /** 115 * Adds the coverage information for the given line. This merges the given 116 * coverage info if called multiple times for the same line. This is needed to 117 * allow for overlapping coverage reports. 118 */ 119 public void addLineCoverage(int line, ELineCoverage coverage) { 120 CCSMAssert.isNotNull(coverage); 121 122 ELineCoverage existingCoverage = getLineCoverage(line); 123 if (existingCoverage == null) { 124 setLineCoverage(line, coverage); 125 return; 126 } 127 128 switch (existingCoverage) { 129 case NOT_COVERED: 130 uncoveredLines.remove(line); 131 setLineCoverage(line, coverage); 132 break; 133 case PARTIALLY_COVERED: 134 if (coverage.equals(ELineCoverage.FULLY_COVERED)) { 135 partiallyCoveredLines.remove(line); 136 fullyCoveredLines.add(line); 137 } 138 break; 139 case FULLY_COVERED: 140 // cannot get any better 141 break; 142 default: 143 throw new IllegalStateException("Unknown line coverage: " + coverage); 144 } 145 146 } 147 148 /** 149 * Adds the coverage information for the given lines. 150 * 151 * @see #addLineCoverage(int, ELineCoverage) 152 */ 153 public void addLineCoverage(Collection<Integer> lines, ELineCoverage coverage) { 154 for (Integer line : lines) { 155 addLineCoverage(line, coverage); 156 } 157 } 158 159 /** 160 * Adds the coverage information for the given lines. 161 * 162 * @see #addLineCoverage(int, ELineCoverage) 163 */ 164 public void addLineCoverage(IntStream lines, ELineCoverage coverage) { 165 lines.forEach(line -> addLineCoverage(line, coverage)); 166 } 167 168 /** Removes all previously stored line coverage for the given line. */ 169 public void removeLineCoverageInfo(int line) { 170 fullyCoveredLines.remove(line); 171 partiallyCoveredLines.remove(line); 172 uncoveredLines.remove(line); 173 } 174 175 /** 176 * Sets the line coverage for the given line. This ignores previously stored 177 * values. 178 */ 179 private void setLineCoverage(int line, ELineCoverage coverage) { 180 switch (coverage) { 181 case FULLY_COVERED: 182 fullyCoveredLines.add(line); 183 break; 184 case PARTIALLY_COVERED: 185 partiallyCoveredLines.add(line); 186 break; 187 case NOT_COVERED: 188 uncoveredLines.add(line); 189 break; 190 default: 191 throw new IllegalStateException("Unknown line coverage: " + coverage); 192 } 193 } 194 195 /** Adds all coverage information from another {@link LineCoverageInfo}. */ 196 public void addAll(LineCoverageInfo coverageInfo) { 197 for (int line : coverageInfo.fullyCoveredLines) { 198 addLineCoverage(line, ELineCoverage.FULLY_COVERED); 199 } 200 for (int line : coverageInfo.partiallyCoveredLines) { 201 addLineCoverage(line, ELineCoverage.PARTIALLY_COVERED); 202 } 203 for (int line : coverageInfo.uncoveredLines) { 204 addLineCoverage(line, ELineCoverage.NOT_COVERED); 205 } 206 } 207 208 /** 209 * Returns the line coverage for the given line or <code>null</code> if none is 210 * stored. 211 */ 212 public ELineCoverage getLineCoverage(int line) { 213 if (fullyCoveredLines.contains(line)) { 214 return ELineCoverage.FULLY_COVERED; 215 } 216 if (partiallyCoveredLines.contains(line)) { 217 return ELineCoverage.PARTIALLY_COVERED; 218 } 219 if (uncoveredLines.contains(line)) { 220 return ELineCoverage.NOT_COVERED; 221 } 222 return null; 223 } 224 225 /** Returns list of fully covered lines (sorted ascending) */ 226 public List<Integer> getFullyCoveredLines() { 227 return CollectionUtils.sort(fullyCoveredLines); 228 } 229 230 /** Returns list of partially covered lines (sorted ascending) */ 231 public List<Integer> getPartiallyCoveredLines() { 232 return CollectionUtils.sort(partiallyCoveredLines); 233 } 234 235 /** Returns list of uncovered lines (sorted ascending) */ 236 public List<Integer> getUncoveredLines() { 237 return CollectionUtils.sort(uncoveredLines); 238 } 239 240 /** Returns the line coverage ratio as a double ([0..1]). */ 241 public double getCoverageRatio() { 242 int lines = getCoverableLines(); 243 if (lines == 0) { 244 return 0; 245 } 246 return getCoveredLines() / lines; 247 } 248 249 /** Returns the number of lines that are covered or partially covered. */ 250 public double getCoveredLines() { 251 return fullyCoveredLines.size() + partiallyCoveredLines.size(); 252 } 253 254 /** Returns the number of lines that are coverable. */ 255 public int getCoverableLines() { 256 return fullyCoveredLines.size() + partiallyCoveredLines.size() + uncoveredLines.size(); 257 } 258 259 /** Returns the set of all lines in the coverage report. */ 260 public Set<Integer> getAllCoverableLines() { 261 return CollectionUtils.unionSet(fullyCoveredLines, partiallyCoveredLines, uncoveredLines); 262 } 263 264 /** 265 * @see #timestamp 266 */ 267 public long getTimestamp() { 268 return timestamp; 269 } 270 271 /** 272 * @see #timestamp 273 */ 274 public void setTimestamp(long timestamp) { 275 this.timestamp = timestamp; 276 } 277 278 /** {@inheritDoc} */ 279 @Override 280 public String toString() { 281 return String.valueOf(getCoverageRatio()); 282 } 283 284 /** Returns a string representation of the covered/uncovered lines. */ 285 public String toLineString() { 286 return "Fully covered: " + StringUtils.concat(CollectionUtils.sort(fullyCoveredLines), ",") 287 + "; partially covered: " + StringUtils.concat(CollectionUtils.sort(partiallyCoveredLines), ",") 288 + "; uncovered: " + StringUtils.concat(CollectionUtils.sort(uncoveredLines), ",") + "; timestamp: " 289 + timestamp; 290 } 291 292 /** 293 * Replaces the coverable lines with the given lines. This also adjusts the 294 * {@link #fullyCoveredLines} and {@link #partiallyCoveredLines} by removing all 295 * lines that are not coverable. 296 */ 297 public void setCoverableLines(Set<Integer> lines) { 298 fullyCoveredLines.retainAll(lines); 299 partiallyCoveredLines.retainAll(lines); 300 301 uncoveredLines.clear(); 302 uncoveredLines.addAll(lines); 303 uncoveredLines.removeAll(fullyCoveredLines); 304 uncoveredLines.removeAll(partiallyCoveredLines); 305 } 306 307 /** 308 * Creates a copy of this object that is stable in regards to serialization, as 309 * it creates the {@link #fullyCoveredLines}, {@link #partiallyCoveredLines} and 310 * {@link #uncoveredLines} sets by adding the respective entries in a sorted 311 * manner. 312 * 313 * This is somewhat of a hack,as this relies on java sets always turning out the 314 * same, if the entries are inserted in the same order. 315 */ 316 public LineCoverageInfo createStableCopy() { 317 LineCoverageInfo copy = new LineCoverageInfo(this.isMethodAccurate); 318 copy.addLineCoverage(getFullyCoveredLines(), ELineCoverage.FULLY_COVERED); 319 copy.addLineCoverage(getPartiallyCoveredLines(), ELineCoverage.PARTIALLY_COVERED); 320 copy.addLineCoverage(getUncoveredLines(), ELineCoverage.NOT_COVERED); 321 return copy; 322 } 323 324 /** 325 * Extends coverage to full entities by using the best covered line for all 326 * lines of an entity. 327 */ 328 public void extendCoverageToStatements(Collection<ShallowEntity> coverableEntities) { 329 for (ShallowEntity entity : coverableEntities) { 330 List<IToken> ownStartTokens = CollectionUtils.filter(entity.ownStartTokens(), 331 token -> token.getType().getTokenClass() != ETokenClass.SYNTHETIC); 332 if (ownStartTokens.isEmpty()) { 333 continue; 334 } 335 336 int startLine = ownStartTokens.get(0).getLineNumber() + 1; 337 int endLine = CollectionUtils.getLast(ownStartTokens).getLineNumber() + 1; 338 if (startLine == endLine) { 339 // no adjustment needed for single line entities 340 continue; 341 } 342 343 if (IntStream.range(startLine, endLine + 1).anyMatch(fullyCoveredLines::contains)) { 344 IntStream.range(startLine, endLine + 1).forEach(fullyCoveredLines::add); 345 IntStream.range(startLine, endLine + 1).forEach(partiallyCoveredLines::remove); 346 IntStream.range(startLine, endLine + 1).forEach(uncoveredLines::remove); 347 } else if (IntStream.range(startLine, endLine + 1).anyMatch(partiallyCoveredLines::contains)) { 348 IntStream.range(startLine, endLine + 1).forEach(partiallyCoveredLines::add); 349 IntStream.range(startLine, endLine + 1).forEach(uncoveredLines::remove); 350 } 351 } 352 } 353}