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}