| // Copyright 2016 The Bazel Authors. All Rights Reserved. |
| // |
| // 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 com.google.testing.coverage; |
| |
| import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Ordering; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| |
| /** Coverage data from the analysis of either a class or a source file. */ |
| public final class CoverageData { |
| private final ImmutableMap<Integer, BranchData> branches; // map of line-number to BranchData |
| private final BitField linesExecuted; |
| private final BitField linesInstrumented; |
| private final ImmutableMap<String, Integer> methodStartLines; |
| private final ImmutableMap<String, Boolean> methodExecuted; |
| |
| private static final BranchData NULL_BRANCH_DATA = BranchData.builder().build(); |
| |
| private CoverageData( |
| ImmutableMap<Integer, BranchData> branches, |
| BitField linesExecuted, |
| BitField linesInstrumented, |
| ImmutableMap<String, Integer> methodStartLines, |
| ImmutableMap<String, Boolean> methodExecuted) { |
| this.branches = branches; |
| this.linesExecuted = linesExecuted; |
| this.linesInstrumented = linesInstrumented; |
| this.methodStartLines = methodStartLines; |
| this.methodExecuted = methodExecuted; |
| } |
| |
| public static Builder builder() { |
| return new Builder(); |
| } |
| |
| /** Returns true if there is no coverage data. */ |
| public boolean isEmpty() { |
| return branches.isEmpty() |
| && linesInstrumented.isEmpty() |
| && methodStartLines.isEmpty() |
| && methodExecuted.isEmpty(); |
| } |
| |
| /** Returns true if the line has branches. */ |
| public boolean hasBranches(Integer line) { |
| return branches.containsKey(line); |
| } |
| |
| /** Gets BranchData for this line */ |
| public BranchData getBranches(Integer line) { |
| if (!hasBranches(line)) { |
| return NULL_BRANCH_DATA; |
| } |
| return branches.get(line); |
| } |
| |
| /** Returns line numbers where more than one branch is present. */ |
| public ImmutableSortedSet<Integer> linesWithBranches() { |
| return branches.entrySet().stream() |
| // Omit branches that are not actually branches (unconditional jumps) |
| .filter(entry -> entry.getValue().size() > 1) |
| .map(Map.Entry::getKey) |
| .collect(toImmutableSortedSet(Ordering.natural())); |
| } |
| |
| /** Returns the names of all methods in this coverage data. */ |
| public ImmutableList<String> getMethods() { |
| return ImmutableList.copyOf(methodStartLines.keySet()); |
| } |
| |
| /** Returns the line number for the first line of the given method. */ |
| public int getMethodLine(String method) { |
| return methodStartLines.get(method); |
| } |
| |
| /** Returns if the given method was executed. */ |
| public boolean isMethodExecuted(String method) { |
| return methodExecuted.get(method); |
| } |
| |
| /** Returns if the given line was executed. */ |
| public boolean isLineExecuted(int line) { |
| return linesExecuted.isBitSet(line); |
| } |
| |
| /** Were any branches on this line executed. */ |
| public boolean isBranchExecuted(int line) { |
| return branches.containsKey(line) && getBranches(line).anyBranchTaken(); |
| } |
| |
| /** Returns the line number for all instrumented lines. */ |
| public ImmutableList<Integer> getInstrumentedLines() { |
| return ImmutableList.copyOf(new InstrumentedLinesIterator(linesInstrumented)); |
| } |
| |
| /** Iterator for instrumented lines. */ |
| private static class InstrumentedLinesIterator implements Iterator<Integer> { |
| |
| private final BitField linesInstrumented; |
| private int idx; |
| private final int maxIdx; |
| |
| InstrumentedLinesIterator(BitField linesInstrumented) { |
| this.linesInstrumented = linesInstrumented; |
| maxIdx = linesInstrumented.sizeInBits(); |
| |
| // Move idx to point to the first populated bit. |
| idx = -1; |
| moveToNextPopulated(); |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return idx < maxIdx; |
| } |
| |
| @Override |
| public Integer next() { |
| // if idx < maxIdx then it points to a valid element |
| if (idx >= maxIdx) { |
| throw new NoSuchElementException(); |
| } |
| int v = idx; |
| moveToNextPopulated(); |
| return v; |
| } |
| |
| private void moveToNextPopulated() { |
| do { |
| idx++; |
| } while (idx < maxIdx && !linesInstrumented.isBitSet(idx)); |
| } |
| } |
| |
| /** Builder for CoverageData. */ |
| public static final class Builder { |
| private final Map<Integer, BranchData.Builder> branches = new HashMap<>(); |
| private final BitField linesExecuted = new BitField(); |
| private final BitField linesInstrumented = new BitField(); |
| private final Map<String, Integer> methodStartLines = new LinkedHashMap<>(); |
| private final Map<String, Boolean> methodExecuted = new LinkedHashMap<>(); |
| |
| /** |
| * Adds a method to the coverage data. |
| * |
| * <p>This overwrites any values for an existing method name. |
| */ |
| @CanIgnoreReturnValue |
| public Builder addMethod(String method, int line, boolean executed) { |
| methodStartLines.put(method, line); |
| methodExecuted.put(method, executed); |
| return this; |
| } |
| |
| /** |
| * Adds a line to the coverage data. |
| * |
| * <p>This overwrites any values for an existing line. |
| */ |
| @CanIgnoreReturnValue |
| public Builder addLine(int line, boolean executed) { |
| linesInstrumented.setBit(line); |
| if (executed) { |
| linesExecuted.setBit(line); |
| } |
| return this; |
| } |
| |
| /** |
| * Adds a branch to the coverage data for the given line. |
| * |
| * <p>Branches for a line are stored in the order they were added by this method. |
| */ |
| @CanIgnoreReturnValue |
| public Builder addBranch(int line, boolean taken) { |
| BranchData.Builder branchBuilder = |
| branches.computeIfAbsent(line, unused -> BranchData.builder()); |
| branchBuilder.addBranch(taken); |
| return this; |
| } |
| |
| /** Returns true if no coverage data has been added. */ |
| public boolean isEmpty() { |
| return branches.isEmpty() |
| && linesInstrumented.isEmpty() |
| && methodStartLines.isEmpty() |
| && methodExecuted.isEmpty(); |
| } |
| |
| /** Returns a new CoverageData from this builder. */ |
| public CoverageData build() { |
| return new CoverageData( |
| ImmutableMap.copyOf(Maps.transformValues(branches, BranchData.Builder::build)), |
| linesExecuted, |
| linesInstrumented, |
| ImmutableMap.copyOf(methodStartLines), |
| ImmutableMap.copyOf(methodExecuted)); |
| } |
| } |
| |
| /** Data for a set of branches on a single line. */ |
| public static final class BranchData { |
| private final BitField branchBits; |
| private final int count; |
| |
| private BranchData(BitField branchBits, int count) { |
| this.branchBits = branchBits; |
| this.count = count; |
| } |
| |
| /** Creates a new Builder for BranchData. */ |
| public static Builder builder() { |
| return new Builder(); |
| } |
| |
| /** Returns true if the branch at the given index was taken. */ |
| public boolean isBranchTaken(int idx) { |
| if (idx >= count) { |
| throw new IndexOutOfBoundsException( |
| String.format("Branch index %d is out of bounds [0, %d)", idx, count)); |
| } |
| return branchBits.isBitSet(idx); |
| } |
| |
| /** Returns true if any branch was taken. */ |
| public boolean anyBranchTaken() { |
| return branchBits.any(); |
| } |
| |
| /** Returns the coverage for each branch on this line. */ |
| public ImmutableList<Boolean> getBranchValues() { |
| ImmutableList.Builder<Boolean> result = ImmutableList.builder(); |
| for (int i = 0; i < count; i++) { |
| result.add(branchBits.isBitSet(i)); |
| } |
| return result.build(); |
| } |
| |
| /** Returns the number of branches on this line. */ |
| public int size() { |
| return count; |
| } |
| |
| /** Returns true if there are no branches on this line. */ |
| public boolean isEmpty() { |
| return count == 0; |
| } |
| |
| /** Builder for BranchData. */ |
| public static class Builder { |
| private final BitField branchBits; |
| private int count; |
| |
| public Builder() { |
| branchBits = new BitField(); |
| } |
| |
| /** |
| * Adds a branch to the BranchData. |
| * |
| * <p>Branches are stored in the order they were added by this method. |
| */ |
| @CanIgnoreReturnValue |
| public Builder addBranch(boolean taken) { |
| if (taken) { |
| branchBits.setBit(count); |
| } |
| count++; |
| return this; |
| } |
| |
| /** Returns a new BranchData from this builder. */ |
| public BranchData build() { |
| return new BranchData(branchBits, count); |
| } |
| } |
| } |
| } |