diff options
author | 2016-12-05 14:36:02 +0000 | |
---|---|---|
committer | 2016-12-05 15:03:03 +0000 | |
commit | af3c41225ed65f801bb2078a12743494411a375a (patch) | |
tree | 5b16d36c5e57a22ddaeef4fba3f79a323c8305a5 | |
parent | 21759e7695e302f18fc60cb9f17a8e68199ceeea (diff) |
Add JacocoCoverageRunner to junitrunner.
(series 3/4 of open-sourcing coverage command for java test)
--
PiperOrigin-RevId: 141046146
MOS_MIGRATED_REVID=141046146
14 files changed, 1514 insertions, 0 deletions
@@ -148,9 +148,11 @@ genrule( "//src/java_tools/singlejar:SingleJar_deploy.jar", "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/genclass:GenClass_deploy.jar", "//src/java_tools/junitrunner/java/com/google/testing/junit/runner:Runner_deploy.jar", + "//src/java_tools/junitrunner/java/com/google/testing/coverage:embedded_tools", "//third_party/ijar", "//third_party/java/apkbuilder:embedded_tools", "//third_party/java/apksig:embedded_tools", + "//third_party/java/jacoco:srcs", ] + select({ ":darwin": [ ":darwin_tools", @@ -284,6 +286,7 @@ filegroup( name = "srcs", srcs = glob(["**"]) + [ "//src/java_tools/buildjar:srcs", + "//src/java_tools/junitrunner/java/com/google/testing/coverage:srcs", "//src/java_tools/junitrunner/java/com/google/testing/junit/junit4:srcs", "//src/java_tools/junitrunner/java/com/google/testing/junit/runner:srcs", "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal:srcs", diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD new file mode 100644 index 0000000000..863bcc060c --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD @@ -0,0 +1,71 @@ +package( + default_visibility = ["//visibility:legacy_public"], +) + +licenses(["notice"]) + +filegroup( + name = "bazel-srcs", + testonly = 0, + srcs = glob([ + "**/*.java", + "BUILD.tools", + ]), + visibility = ["//third_party/bazel:__subpackages__"], +) + +filegroup( + name = "srcs", + srcs = glob(["**"]), +) + +filegroup( + name = "embedded_tools", + srcs = [ + "BUILD.tools", + "JacocoCoverage_deploy.jar", + ], +) + +filegroup( + name = "jacoco_coverage_runtime", + srcs = ["JacocoCoverage_deploy.jar"], +) + +# Bazel custom Jacoco runner used to provide proper initialization and lcov +# report generation when using offline Jacoco instrumentation. +# This target should not be used as a dependency (except when writing tests for +# it). +# +# An implicit dependency of all "java_binary" rules. +java_binary( + name = "JacocoCoverage", + srcs = [ + "BranchCoverageDetail.java", + "BranchDetailAnalyzer.java", + "BranchExp.java", + "ClassProbesMapper.java", + "CovExp.java", + "JacocoCoverageRunner.java", + "JacocoLCOVFormatter.java", + "MethodProbesMapper.java", + "ProbeExp.java", + ], + deps = [ + ":bitfield", + "//third_party/java/jacoco:blaze-agent-neverlink", + "//third_party/java/jacoco:core", + "//third_party/java/jacoco:report", + ], +) + +java_library( + name = "bitfield", + srcs = [ + "BitField.java", + "IllegalStringException.java", + ], + deps = [ + "//third_party:apache_commons_lang", + ], +) diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD.tools b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD.tools new file mode 100644 index 0000000000..c52cacce2a --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD.tools @@ -0,0 +1,8 @@ +package( + default_visibility = ["//visibility:public"], +) + +java_import( + name = "JacocoCoverage", + jars = ["JacocoCoverage_deploy.jar"], +) diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BitField.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/BitField.java new file mode 100644 index 0000000000..5967677578 --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BitField.java @@ -0,0 +1,209 @@ +// 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 java.util.Arrays; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + +/** + * Abstracts bit field operations. + */ +public class BitField { + + private byte[] bytes; + + private static final int[] BIT_COUNT_LOOKUP = new int[256]; + + static { + BIT_COUNT_LOOKUP[0] = 0; + BIT_COUNT_LOOKUP[1] = 1; + for (int i = 2; i < 256; i += 2) { + int count = BIT_COUNT_LOOKUP[i / 2]; + BIT_COUNT_LOOKUP[i] = count; + BIT_COUNT_LOOKUP[i + 1] = count + 1; + } + } + + public BitField() { + this(new byte[0]); + } + + public BitField(byte[] bytes) { + this.bytes = bytes.clone(); + } + + /** + * Returns a copy of the underlying byte array. + * + * @return byte array copy + */ + public byte[] getBytes() { + return bytes.clone(); + } + + /** + * Sets or clears a bit at the given index. + * + * @param index bit index + */ + public void setBit(int index) { + setBit(index, true); + } + + /** + * Clears a bit at the given index + * + * @param index bit index + */ + public void clearBit(int index) { + setBit(index, false); + } + + /** + * Sets or clears a bit at the given index. + * + * @param index bit index + */ + private void setBit(int index, boolean isSet) { + int byteIndex = index / 8; + int newByteSize = byteIndex + 1; + if (bytes.length < newByteSize) { + bytes = Arrays.copyOf(bytes, newByteSize); + } + + int bitIndex = index % 8; + int mask = 1 << bitIndex; + + if (isSet) { + bytes[byteIndex] = (byte) (bytes[byteIndex] | mask); + } else { + bytes[byteIndex] = (byte) (bytes[byteIndex] & ~mask); + } + } + + /** + * Checks whether a bit at the given index is set. + * + * @param index bit index + * @return true if set, false otherwise + */ + public boolean isBitSet(int index) { + int byteIndex = index / 8; + + if (byteIndex >= bytes.length) { + return false; + } + + int bitIndex = index % 8; + int mask = 1 << bitIndex; + return (bytes[byteIndex] & mask) != 0; + } + + /** Performs a non-destructive bit-wise "and" of this bit field with another one. */ + public BitField and(BitField other) { + int size = Math.min(bytes.length, other.bytes.length); + byte[] result = new byte[size]; + + for (int i = 0; i < size; i++) { + result[i] = (byte) (bytes[i] & other.bytes[i]); + } + + return new BitField(result); + } + + /** + * Performs a non-destructive bit-wise merge of this bit field and another one. + * + * @param other the other bit field + * @return this bit field + */ + public BitField or(BitField other) { + byte[] largerArray, smallerArray; + if (bytes.length < other.bytes.length) { + largerArray = other.bytes; + smallerArray = bytes; + } else { + largerArray = bytes; + smallerArray = other.bytes; + } + + // Start out with a copy of the larger of the two arrays. + byte[] result = Arrays.copyOf(largerArray, largerArray.length); + + for (int i = 0; i < smallerArray.length; i++) { + result[i] |= smallerArray[i]; + } + + return new BitField(result); + } + + /** + * Compares two bit fields for equality. + * + * @param obj another object + * @return true if the other object is a bit field with the same bits set + */ + @Override + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(this, obj); + } + + /** + * Compare a BitField object with an array of bytes + * + * @param other a byte array to compare to + * @return true if the underlying byte array is equal to the given byte array + */ + public boolean equals(byte[] other) { + return Arrays.equals(bytes, other); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + public int countBitsSet() { + int count = 0; + for (byte b : bytes) { + // JAVA doesn't have the concept of unsigned byte; need to & with 255 + // to avoid exception of IndexOutOfBoundException when b < 0. + count += BIT_COUNT_LOOKUP[0xFF & b]; + } + return count; + } + + public BitField not() { + byte[] invertedBytes = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + invertedBytes[i] = Integer.valueOf(~bytes[i]).byteValue(); + } + return new BitField(invertedBytes); + } + + public int sizeInBits() { + return bytes.length * 8; + } + + public boolean any() { + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] != (byte) 0) { + return true; + } + } + return false; + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchCoverageDetail.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchCoverageDetail.java new file mode 100644 index 0000000000..6b56a1cff8 --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchCoverageDetail.java @@ -0,0 +1,86 @@ +// 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 java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** Details of branch coverage information. */ +public class BranchCoverageDetail { + private final Map<Integer, BitField> branchTaken; + private final Map<Integer, Integer> branches; + + public BranchCoverageDetail() { + branchTaken = new TreeMap<Integer, BitField>(); + branches = new TreeMap<Integer, Integer>(); + } + + private BitField getBranchForLine(int line) { + BitField value = branchTaken.get(line); + if (value != null) { + return value; + } + value = new BitField(); + branchTaken.put(line, value); + return value; + } + + /** Returns true if the line has branches. */ + public boolean hasBranches(int line) { + return branches.containsKey(line); + } + + /** Sets the number of branches entry. */ + public void setBranches(int line, int n) { + branches.put(line, n); + } + + /** Gets the number of branches in the line, returns 0 if there is no branch. */ + public int getBranches(int line) { + Integer value = branches.get(line); + if (value == null) { + return 0; + } + return value; + } + + /** Sets the taken bit of the given line for the given branch index. */ + public void setTakenBit(int line, int branchIdx) { + getBranchForLine(line).setBit(branchIdx); + } + + public boolean getTakenBit(int line, int branchIdx) { + return getBranchForLine(line).isBitSet(branchIdx); + } + + /** Calculate executed bit using heuristics. */ + public boolean getExecutedBit(int line) { + // If any of the branch is taken, the branch must have executed. Otherwise assume it is not. + return getBranchForLine(line).any(); + } + + /** Returns line numbers where more than one branch is present. */ + public Set<Integer> linesWithBranches() { + Set<Integer> result = new TreeSet<Integer>(); + for (int i : branches.keySet()) { + if (branches.get(i) > 1) { + result.add(i); + } + } + return result; + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchDetailAnalyzer.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchDetailAnalyzer.java new file mode 100644 index 0000000000..6e85b9fe26 --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchDetailAnalyzer.java @@ -0,0 +1,86 @@ +// 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 java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.jacoco.core.analysis.Analyzer; +import org.jacoco.core.data.ExecutionData; +import org.jacoco.core.data.ExecutionDataStore; +import org.jacoco.core.internal.data.CRC64; +import org.jacoco.core.internal.flow.ClassProbesAdapter; +import org.objectweb.asm.ClassReader; + +/** + * Analyzer that process the branch coverage detail information. + * + * <p>Reuse the Analyzer class from Jacoco to avoid duplicating the content detection logic. + * Override the main {@code Analyzer.analyzeClass} method which does the main work. + */ +public class BranchDetailAnalyzer extends Analyzer { + + private final ExecutionDataStore executionData; + private final Map<String, BranchCoverageDetail> branchDetails; + + public BranchDetailAnalyzer(final ExecutionDataStore executionData) { + super(executionData, null); + this.executionData = executionData; + this.branchDetails = new TreeMap<String, BranchCoverageDetail>(); + } + + @Override + public void analyzeClass(final ClassReader reader) { + final Map<Integer, BranchExp> lineToBranchExp = mapProbes(reader); + + long classid = CRC64.checksum(reader.b); + ExecutionData classData = executionData.get(classid); + if (classData == null) { + return; + } + boolean[] probes = classData.getProbes(); + + BranchCoverageDetail detail = new BranchCoverageDetail(); + + for (Map.Entry<Integer, BranchExp> entry : lineToBranchExp.entrySet()) { + int line = entry.getKey(); + BranchExp branchExp = entry.getValue(); + List<CovExp> branches = branchExp.getBranches(); + + detail.setBranches(line, branches.size()); + for (int branchIdx = 0; branchIdx < branches.size(); branchIdx++) { + if (branches.get(branchIdx).eval(probes)) { + detail.setTakenBit(line, branchIdx); + } + } + } + if (detail.linesWithBranches().size() > 0) { + branchDetails.put(reader.getClassName(), detail); + } + } + + // Generate the line to probeExp map so that we can evaluate the coverage. + private Map<Integer, BranchExp> mapProbes(final ClassReader reader) { + final ClassProbesMapper mapper = new ClassProbesMapper(); + final ClassProbesAdapter adapter = new ClassProbesAdapter(mapper, false); + reader.accept(adapter, 0); + + return mapper.result(); + } + + public Map<String, BranchCoverageDetail> getBranchDetails() { + return branchDetails; + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchExp.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchExp.java new file mode 100644 index 0000000000..f6b1f9be7c --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchExp.java @@ -0,0 +1,81 @@ +// 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 java.util.ArrayList; +import java.util.List; + +/** A branch coverage that must be evaluated as a combination of probes. */ +public class BranchExp implements CovExp { + private final List<CovExp> branches; + + private boolean hasValue; + private boolean[] probesUsed; + private boolean value; + + public BranchExp(List<CovExp> branches) { + this.branches = branches; + hasValue = false; + } + + /** Create a new BranchExp using this CovExp as the only branch. */ + public BranchExp(CovExp exp) { + branches = new ArrayList<CovExp>(); + branches.add(exp); + hasValue = false; + } + + /** Gets the expressions for the branches. */ + public List<CovExp> getBranches() { + return branches; + } + + /** + * Add an expression to a branch expression. + * + * @return the index of the newly added branch. + */ + public int add(CovExp exp) { + branches.add(exp); + return branches.size() - 1; + } + + /** Update an existing branch expression. */ + public void update(int idx, CovExp exp) { + branches.set(idx, exp); + } + + /** Make a union of the the branches of two BranchExp. */ + public void merge(BranchExp other) { + branches.addAll(other.branches); + } + + @Override + public boolean eval(final boolean[] probes) { + if (hasValue && probes == probesUsed) { + return value; + } + value = false; + for (CovExp exp : branches) { + value = exp.eval(probes); + if (value) { + break; + } + } + hasValue = value; // The value is cached. + probesUsed = probes; + return value; + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/ClassProbesMapper.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/ClassProbesMapper.java new file mode 100644 index 0000000000..ed65bf184b --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/ClassProbesMapper.java @@ -0,0 +1,59 @@ +// 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 java.util.Map; +import java.util.TreeMap; +import org.jacoco.core.internal.flow.ClassProbesVisitor; +import org.jacoco.core.internal.flow.MethodProbesVisitor; +import org.objectweb.asm.FieldVisitor; + +/** A visitor that maps each source code line to the probes corresponding to the lines. */ +public class ClassProbesMapper extends ClassProbesVisitor { + private Map<Integer, BranchExp> classLineToBranchExp; + + public Map<Integer, BranchExp> result() { + return classLineToBranchExp; + } + + /** Create a new probe mapper object. */ + public ClassProbesMapper() { + classLineToBranchExp = new TreeMap<Integer, BranchExp>(); + } + + /** Returns a visitor for mapping method code. */ + @Override + public MethodProbesVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + return new MethodProbesMapper() { + @Override + public void visitEnd() { + super.visitEnd(); + classLineToBranchExp.putAll(result()); + } + }; + } + + @Override + public FieldVisitor visitField( + int access, String name, String desc, String signature, Object value) { + return super.visitField(access, name, desc, signature, value); + } + + @Override + public void visitTotalProbeCount(int count) { + // Nothing to do. Maybe perform some sanity checks here. + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/CovExp.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/CovExp.java new file mode 100644 index 0000000000..d5a56a5351 --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/CovExp.java @@ -0,0 +1,24 @@ +// 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; + +/** + * Expressions to evaluate a branch coverage result, either a simple probeId or a combination of + * probes. + */ +public interface CovExp { + /** Evaluate the expression using the given values of probes. */ + public abstract boolean eval(final boolean[] probes); +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/IllegalStringException.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/IllegalStringException.java new file mode 100644 index 0000000000..7d6a7d8c54 --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/IllegalStringException.java @@ -0,0 +1,26 @@ +// 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; + +/** + * Thrown when BitField.fromString() is called with a string with either odd number of or + * non-hexadecimal characters + */ +public class IllegalStringException extends RuntimeException { + + public IllegalStringException(String message) { + super(message); + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java new file mode 100644 index 0000000000..3ef86a006f --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java @@ -0,0 +1,278 @@ +// 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 java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.jar.Attributes; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import org.jacoco.agent.rt.IAgent; +import org.jacoco.agent.rt.RT; +import org.jacoco.core.analysis.Analyzer; +import org.jacoco.core.analysis.CoverageBuilder; +import org.jacoco.core.analysis.IBundleCoverage; +import org.jacoco.core.tools.ExecFileLoader; +import org.jacoco.report.IReportVisitor; +import org.jacoco.report.ISourceFileLocator; + +/** + * Runner class used to generate code coverage report when using Jacoco offline instrumentation. + * + * <p>The complete list of features available for Jacoco offline instrumentation: + * http://www.eclemma.org/jacoco/trunk/doc/offline.html + * + * <p>The structure is roughly following the canonical Jacoco example: + * http://www.eclemma.org/jacoco/trunk/doc/examples/java/ReportGenerator.java + * + * <p>The following environment variables are expected: + * JAVA_COVERAGE_FILE - specifies final location of the generated lcov file. + * JACOCO_METADATA_JAR - specifies jar containing uninstrumented classes to be analyzed. + */ +public class JacocoCoverageRunner { + + private final List<File> classesJars; + private final InputStream executionData; + private final File reportFile; + private ExecFileLoader execFileLoader; + + public JacocoCoverageRunner(InputStream jacocoExec, String reportPath, File... metadataJars) { + executionData = jacocoExec; + reportFile = new File(reportPath); + + classesJars = new ArrayList<>(); + for (File metadataJar : metadataJars) { + classesJars.add(metadataJar); + } + } + + public void create() throws IOException { + // Read the jacoco.exec file. Multiple data files could be merged at this point + execFileLoader = new ExecFileLoader(); + execFileLoader.load(executionData); + + // Run the structure analyzer on a single class folder or jar file to build up the coverage + // model. Typically you would create a bundle for each class folder and each jar you want in + // your report. If you have more than one bundle you may need to add a grouping node to the + // report. The lcov formatter doesn't seem to care, and we're only using one bundle anyway. + final IBundleCoverage bundleCoverage = analyzeStructure(); + + final Map<String, BranchCoverageDetail> branchDetails = analyzeBranch(); + createReport(bundleCoverage, branchDetails); + } + + private void createReport( + final IBundleCoverage bundleCoverage, final Map<String, BranchCoverageDetail> branchDetails) + throws IOException { + JacocoLCOVFormatter formatter = new JacocoLCOVFormatter(); + final IReportVisitor visitor = formatter.createVisitor(reportFile, branchDetails); + + // Initialize the report with all of the execution and session information. At this point the + // report doesn't know about the structure of the report being created. + visitor.visitInfo( + execFileLoader.getSessionInfoStore().getInfos(), + execFileLoader.getExecutionDataStore().getContents()); + + // Populate the report structure with the bundle coverage information. + // Call visitGroup if you need groups in your report. + + // Note the API requires a sourceFileLocator because the HTML and XML formatters display a page + // of code annotated with coverage information. Having the source files is not actually needed + // for generating the lcov report... + visitor.visitBundle( + bundleCoverage, + new ISourceFileLocator() { + + @Override + public Reader getSourceFile(String packageName, String fileName) throws IOException { + return null; + } + + @Override + public int getTabWidth() { + return 0; + } + }); + + // Signal end of structure information to allow report to write all information out + visitor.visitEnd(); + } + + private IBundleCoverage analyzeStructure() throws IOException { + final CoverageBuilder coverageBuilder = new CoverageBuilder(); + final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder); + for (File classesJar : classesJars) { + analyzer.analyzeAll(classesJar); + } + + // TODO(bazel-team): Find out where the name of the bundle can pop out in the report. + return coverageBuilder.getBundle("isthisevenused"); + } + + // Additional pass to process the branch details of the classes + private Map<String, BranchCoverageDetail> analyzeBranch() throws IOException { + final BranchDetailAnalyzer analyzer = + new BranchDetailAnalyzer(execFileLoader.getExecutionDataStore()); + + Map<String, BranchCoverageDetail> result = new TreeMap<>(); + for (File classesJar : classesJars) { + analyzer.analyzeAll(classesJar); + result.putAll(analyzer.getBranchDetails()); + } + return result; + } + + private static String getMainClass(String metadataJar) throws Exception { + if (metadataJar != null) { + // Blaze guarantees that JACOCO_METADATA_JAR has a proper manifest with a Main-Class entry. + try (JarInputStream jarStream = new JarInputStream(new FileInputStream(metadataJar))) { + return jarStream.getManifest().getMainAttributes().getValue("Main-Class"); + } + } else { + // If metadataJar was not set, we're running inside a deploy jar. We have to open the manifest + // and read the value of "Precoverage-Class", set by Blaze. Note ClassLoader#getResource() + // will only return the first result, most likely a manifest from the bootclasspath. + Enumeration<URL> manifests = + JacocoCoverageRunner.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); + while (manifests.hasMoreElements()) { + Manifest manifest = new Manifest(manifests.nextElement().openStream()); + Attributes attributes = manifest.getMainAttributes(); + String className = attributes.getValue("Coverage-Main-Class"); + if (className != null) { + return className; + } + } + throw new IllegalStateException( + "JACOCO_METADATA_JAR environment variable is not set, and no" + + " META-INF/MANIFEST.MF on the classpath has a Coverage-Main-Class attribute. " + + " Cannot determine the name of the main class for the code under test."); + } + } + + private static String getUniquePath(String pathTemplate, String suffix) throws IOException { + // If pathTemplate is null, we're likely executing from a deploy jar and the test framework + // did not properly set the environment for coverage reporting. This alone is not a reason for + // throwing an exception, we're going to run anyway and write the coverage data to a temporary, + // throw-away file. + if (pathTemplate == null) { + return File.createTempFile("coverage", suffix).getPath(); + } else { + // Blaze sets the path template to a file with the .dat extension. lcov_merger matches all + // files having '.dat' in their name, so instead of appending we change the extension. + File absolutePathTemplate = new File(pathTemplate).getAbsoluteFile(); + String prefix = absolutePathTemplate.getName(); + int lastDot = prefix.lastIndexOf('.'); + if (lastDot != -1) { + prefix = prefix.substring(0, lastDot); + } + return File.createTempFile(prefix, suffix, absolutePathTemplate.getParentFile()).getPath(); + } + } + + public static void main(String[] args) throws Exception { + final String metadataJar = System.getenv("JACOCO_METADATA_JAR"); + final String coverageReportBase = System.getenv("JAVA_COVERAGE_FILE"); + + // Disable Jacoco's default output mechanism, which runs as a shutdown hook. We generate the + // report in our own shutdown hook below, and we want to avoid the data race (shutdown hooks are + // not guaranteed any particular order). Note that also by default, Jacoco appends coverage + // data, which can have surprising results if running tests locally or somehow encountering + // the previous .exec file. + System.setProperty("jacoco-agent.output", "none"); + + // We have no use for this sessionId property, but leaving it blank results in a DNS lookup + // at runtime. A minor annoyance: the documentation insists the property name is "sessionId", + // however on closer inspection of the source code, it turns out to be "sessionid"... + System.setProperty("jacoco-agent.sessionid", "default"); + + // A JVM shutdown hook has a fixed amount of time (OS-dependent) before it is terminated. + // For our purpose, it's more than enough to scan through the instrumented jar and match up + // the bytecode with the coverage data. It wouldn't be enough for scanning the entire classpath, + // or doing something else terribly inefficient. + Runtime.getRuntime() + .addShutdownHook( + new Thread() { + @Override + public void run() { + try { + // If the test spawns multiple JVMs, they will race to write to the same files. We + // need to generate unique paths for each execution. lcov_merger simply collects + // all the .dat files in the current directory anyway, so we don't need to worry + // about merging them. + String coverageReport = getUniquePath(coverageReportBase, ".dat"); + String coverageData = getUniquePath(coverageReportBase, ".exec"); + + // Get a handle on the Jacoco Agent and write out the coverage data. Other options + // included talking to the agent via TCP (useful when gathering coverage from + // multiple JVMs), or via JMX (the agent's MXBean is called + // 'org.jacoco:type=Runtime'). As we're running in the same JVM, these options + // seemed overkill, we can just refer to the Jacoco runtime as RT. + // See http://www.eclemma.org/jacoco/trunk/doc/agent.html for all the options + // available. + ByteArrayInputStream dataInputStream; + try { + IAgent agent = RT.getAgent(); + byte[] data = agent.getExecutionData(false); + try (FileOutputStream fs = new FileOutputStream(coverageData, true)) { + fs.write(data); + } + // We append to the output file, but run report generation only for the coverage + // data from this JVM. The output file may contain data from other + // subprocesses, etc. + dataInputStream = new ByteArrayInputStream(data); + } catch (IllegalStateException e) { + // In this case, we didn't execute a single instrumented file, so the agent + // isn't live. There's no coverage to report, but it's otherwise a successful + // invocation. + dataInputStream = new ByteArrayInputStream(new byte[0]); + } + + if (metadataJar != null) { + // Disable coverage in this case. The build system should report an error or + // warning if this happens. It's too late at this point. + new JacocoCoverageRunner(dataInputStream, coverageReport, new File(metadataJar)) + .create(); + } + } catch (IOException e) { + e.printStackTrace(); + Runtime.getRuntime().halt(1); + } + } + }); + + // Another option would be to run the tests in a separate JVM, let Jacoco dump out the coverage + // data, wait for the subprocess to finish and then generate the lcov report. The only benefit + // of doing this is not being constrained by the hard 5s limit of the shutdown hook. Setting up + // the subprocess to match all JVM flags, runtime classpath, bootclasspath, etc is doable. + // We'd share the same limitation if the system under test uses shutdown hooks internally, as + // there's no way to collect coverage data on that code. + String mainClass = getMainClass(metadataJar); + Method main = + Class.forName(mainClass).getMethod("main", new Class[] {java.lang.String[].class}); + main.invoke(null, new Object[] {args}); + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoLCOVFormatter.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoLCOVFormatter.java new file mode 100644 index 0000000000..8c991a11d0 --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoLCOVFormatter.java @@ -0,0 +1,151 @@ +// 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 java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.jacoco.core.analysis.IBundleCoverage; +import org.jacoco.core.analysis.IClassCoverage; +import org.jacoco.core.analysis.ICounter; +import org.jacoco.core.analysis.IMethodCoverage; +import org.jacoco.core.analysis.IPackageCoverage; +import org.jacoco.core.analysis.ISourceFileCoverage; +import org.jacoco.core.data.ExecutionData; +import org.jacoco.core.data.SessionInfo; +import org.jacoco.report.IReportGroupVisitor; +import org.jacoco.report.IReportVisitor; +import org.jacoco.report.ISourceFileLocator; + +/** + * Simple lcov formatter to be used with lcov_merger.par. + * + * <p>The lcov format is documented here: http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php + */ +public class JacocoLCOVFormatter { + + public IReportVisitor createVisitor( + final File output, final Map<String, BranchCoverageDetail> branchCoverageDetail) { + return new IReportVisitor() { + + private Map<String, Map<String, IClassCoverage>> sourceToClassCoverage = new TreeMap<>(); + private Map<String, ISourceFileCoverage> sourceToFileCoverage = new TreeMap<>(); + + @Override + public void visitInfo(List<SessionInfo> sessionInfos, Collection<ExecutionData> executionData) + throws IOException {} + + @Override + public void visitEnd() throws IOException { + try (FileWriter fileWriter = new FileWriter(output, true); + PrintWriter printWriter = new PrintWriter(fileWriter)) { + for (String sourceFile : sourceToClassCoverage.keySet()) { + processSourceFile(printWriter, sourceFile); + } + } + } + + @Override + public void visitBundle(IBundleCoverage bundle, ISourceFileLocator locator) + throws IOException { + // Jacoco's API is geared towards HTML/XML reports which have a hierarchical nature. The + // following loop would call the report generators for packages, classes, methods, and + // finally link the source view (which would be generated by walking the actual source file + // and annotating the coverage data). For lcov, we don't really need the source file, but + // we need to output FN/FNDA pairs with method coverage, which means we need to index this + // information and process everything at the end. + for (IPackageCoverage pkgCoverage : bundle.getPackages()) { + for (IClassCoverage clsCoverage : pkgCoverage.getClasses()) { + String fileName = clsCoverage.getPackageName() + "/" + clsCoverage.getSourceFileName(); + if (!sourceToClassCoverage.containsKey(fileName)) { + sourceToClassCoverage.put(fileName, new TreeMap<String, IClassCoverage>()); + } + sourceToClassCoverage.get(fileName).put(clsCoverage.getName(), clsCoverage); + } + for (ISourceFileCoverage srcCoverage : pkgCoverage.getSourceFiles()) { + sourceToFileCoverage.put( + srcCoverage.getPackageName() + "/" + srcCoverage.getName(), srcCoverage); + } + } + } + + @Override + public IReportGroupVisitor visitGroup(String name) throws IOException { + return null; + } + + private void processSourceFile(PrintWriter writer, String sourceFile) { + writer.printf("SF:%s\n", sourceFile); + + ISourceFileCoverage srcCoverage = sourceToFileCoverage.get(sourceFile); + if (srcCoverage != null) { + // List methods, including methods from nested classes, in FN/FNDA pairs + for (IClassCoverage clsCoverage : sourceToClassCoverage.get(sourceFile).values()) { + for (IMethodCoverage mthCoverage : clsCoverage.getMethods()) { + String name = constructFunctionName(mthCoverage, clsCoverage.getName()); + writer.printf("FN:%d,%s\n", mthCoverage.getFirstLine(), name); + writer.printf("FNDA:%d,%s\n", mthCoverage.getMethodCounter().getCoveredCount(), name); + } + } + + for (IClassCoverage clsCoverage : sourceToClassCoverage.get(sourceFile).values()) { + BranchCoverageDetail detail = branchCoverageDetail.get(clsCoverage.getName()); + if (detail != null) { + for (int line : detail.linesWithBranches()) { + int numBranches = detail.getBranches(line); + boolean executed = detail.getExecutedBit(line); + if (executed) { + for (int branchIdx = 0; branchIdx < numBranches; branchIdx++) { + if (detail.getTakenBit(line, branchIdx)) { + writer.printf("BA:%d,%d\n", line, 2); // executed, taken + } else { + writer.printf("BA:%d,%d\n", line, 1); // executed, not taken + } + } + } else { + for (int branchIdx = 0; branchIdx < numBranches; branchIdx++) { + writer.printf("BA:%d,%d\n", line, 0); // not executed + } + } + } + } + } + + // List of DA entries matching source lines + int firstLine = srcCoverage.getFirstLine(); + int lastLine = srcCoverage.getLastLine(); + for (int line = firstLine; line <= lastLine; line++) { + ICounter instructionCounter = srcCoverage.getLine(line).getInstructionCounter(); + if (instructionCounter.getTotalCount() != 0) { + writer.printf("DA:%d,%d\n", line, instructionCounter.getCoveredCount()); + } + } + } + writer.println("end_of_record"); + } + + private String constructFunctionName(IMethodCoverage mthCoverage, String clsName) { + // The lcov spec doesn't of course cover Java formats, so we output the method signature. + // lcov_merger doesn't seem to care about these entries. + return clsName + "::" + mthCoverage.getName() + " " + mthCoverage.getDesc(); + } + }; + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/MethodProbesMapper.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/MethodProbesMapper.java new file mode 100644 index 0000000000..6900a37f71 --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/MethodProbesMapper.java @@ -0,0 +1,403 @@ +// 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.jacoco.core.internal.flow.IFrame; +import org.jacoco.core.internal.flow.Instruction; +import org.jacoco.core.internal.flow.LabelInfo; +import org.jacoco.core.internal.flow.MethodProbesVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; + +/** + * The mapper is a probes visitor that will cache control flow information as well as keeping track + * of the probes as the main driver generates the probe ids. Upon finishing the method it uses the + * information collected to generate the mapping information between probes and the instructions. + */ +public class MethodProbesMapper extends MethodProbesVisitor { + /* + * The implementation roughly follows the same pattern of the Analyzer class of Jacoco. + * + * The mapper has a few states: + * + * - lineMappings: a mapping between line number and labels + * + * - a sequence of "instructions", where each instruction has one or more predecessors. The + * predecessor field has a sole purpose of propagating probe id. The 'merge' nodes in the CFG has + * no predecessors, since the branch stops at theses points. + * + * - The instructions each has states that keep track of the probes that are associated with the + * instruction. + * + * Initially the probe ids are assigned to the instructions that immediately precede the probe. At + * the end of visiting the methods, the probe ids are propagated through the predecessor chains. + */ + + // States + // + // These are state variables that needs to be updated in the visitor methods. + // The values usually changes as we traverse the byte code. + private Instruction lastInstruction = null; + private int currentLine = -1; + private List<Label> currentLabels = new ArrayList<>(); + + // Result + private Map<Integer, BranchExp> lineToBranchExp = new TreeMap(); + + public Map<Integer, BranchExp> result() { + return lineToBranchExp; + } + + // Intermediate results + // + // These values are built up during the visitor methods. They will be used to compute + // the final results. + private List<Instruction> instructions = new ArrayList<Instruction>(); + private List<Jump> jumps = new ArrayList<>(); + private Map<Integer, Instruction> probeToInsn = new TreeMap<>(); + + // A map which associates intructions with their coverage expressions. + private final Map<Instruction, CovExp> insnToCovExp = new HashMap(); + + // A map which associates a instruction to the branch index in its predecessor + // e.g., the instruction that follows a conditional jump instruction must exists in + // this map. + private final Map<Instruction, Integer> insnToIdx = new HashMap(); + + // Local cache + // + // These are maps corresponding to data structures available in JaCoCo in other form. + // We use local data structure to avoid need to change the JaCoCo internal code. + private Map<Instruction, Instruction> predecessors = new HashMap<>(); + private Map<Label, Instruction> labelToInsn = new HashMap<>(); + + /** Visitor method to append a new Instruction */ + private void visitInsn() { + Instruction instruction = new Instruction(currentLine); + instructions.add(instruction); + if (lastInstruction != null) { + instruction.setPredecessor(lastInstruction); // Update branch of lastInstruction + predecessors.put(instruction, lastInstruction); // Update local cache + } + + for (Label label : currentLabels) { + labelToInsn.put(label, instruction); + } + currentLabels.clear(); // Update states + lastInstruction = instruction; + } + + // Plain visitors: called from adapter when no probe is needed + @Override + public void visitInsn(int opcode) { + visitInsn(); + } + + @Override + public void visitIntInsn(int opcode, int operand) { + visitInsn(); + } + + @Override + public void visitVarInsn(int opcode, int variable) { + visitInsn(); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + visitInsn(); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + visitInsn(); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + visitInsn(); + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle handle, Object... args) { + visitInsn(); + } + + @Override + public void visitLdcInsn(Object cst) { + visitInsn(); + } + + @Override + public void visitIincInsn(int var, int inc) { + visitInsn(); + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + visitInsn(); + } + + // Methods that need to update the states + @Override + public void visitJumpInsn(int opcode, Label label) { + visitInsn(); + jumps.add(new Jump(lastInstruction, label)); + } + + @Override + public void visitLabel(Label label) { + currentLabels.add(label); + if (!LabelInfo.isSuccessor(label)) { + lastInstruction = null; + } + } + + @Override + public void visitLineNumber(int line, Label start) { + currentLine = line; + } + + /** Visit a switch instruction with no probes */ + private void visitSwitchInsn(Label dflt, Label[] labels) { + visitInsn(); + + // Handle default transition + LabelInfo.resetDone(dflt); + jumps.add(new Jump(lastInstruction, dflt)); + LabelInfo.setDone(dflt); + + // Handle other transitions + LabelInfo.resetDone(labels); + for (Label label : labels) { + if (!LabelInfo.isDone(label)) { + jumps.add(new Jump(lastInstruction, label)); + LabelInfo.setDone(label); + } + } + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + visitSwitchInsn(dflt, labels); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + visitSwitchInsn(dflt, labels); + } + + private void addProbe(int probeId) { + // We do not add probes to the flow graph, but we need to update + // the branch count of the predecessor of the probe + lastInstruction.addBranch(); + probeToInsn.put(probeId, lastInstruction); + } + + // Probe visit methods + @Override + public void visitProbe(int probeId) { + // This function is only called when visiting a merge node which + // is a successor. + // It adds an probe point to the last instruction + assert (lastInstruction != null); + + addProbe(probeId); + lastInstruction = null; // Merge point should have no predecessor. + } + + @Override + public void visitJumpInsnWithProbe(int opcode, Label label, int probeId, IFrame frame) { + visitInsn(); + addProbe(probeId); + } + + @Override + public void visitInsnWithProbe(int opcode, int probeId) { + visitInsn(); + addProbe(probeId); + } + + @Override + public void visitTableSwitchInsnWithProbes( + int min, int max, Label dflt, Label[] labels, IFrame frame) { + visitSwitchInsnWithProbes(dflt, labels); + } + + @Override + public void visitLookupSwitchInsnWithProbes( + Label dflt, int[] keys, Label[] labels, IFrame frame) { + visitSwitchInsnWithProbes(dflt, labels); + } + + private void visitSwitchInsnWithProbes(Label dflt, Label[] labels) { + visitInsn(); + LabelInfo.resetDone(dflt); + LabelInfo.resetDone(labels); + + visitTargetWithProbe(dflt); + for (Label l : labels) { + visitTargetWithProbe(l); + } + } + + private void visitTargetWithProbe(Label label) { + if (!LabelInfo.isDone(label)) { + int id = LabelInfo.getProbeId(label); + if (id == LabelInfo.NO_PROBE) { + jumps.add(new Jump(lastInstruction, label)); + } else { + // Note, in this case the instrumenter should insert intermediate labels + // for the probes. These probes will be added for the switch instruction. + // + // There is no direct jump between lastInstruction and the label either. + addProbe(id); + } + LabelInfo.setDone(label); + } + } + + // If a CovExp of pred is ProbeExp, create a single-branch BranchExp and put it in the map. + // Also update the index of insn. + private BranchExp getPredBranchExp(Instruction predecessor, Instruction insn) { + BranchExp result = null; + CovExp exp = insnToCovExp.get(predecessor); + if (exp instanceof ProbeExp) { + result = new BranchExp(exp); // Change ProbeExp to BranchExp + insnToCovExp.put(predecessor, result); + // This can only happen if the internal data of Jacoco is inconsistent: + // the instruction is the predecessor of more than one instructions, + // but its branch count is not > 1. + } else { + result = (BranchExp) exp; + } + return result; + } + + // Update a branch predecessor and returns whether the BranchExp of the predecessor is new. + private boolean updateBranchPredecessor(Instruction predecessor, Instruction insn, CovExp exp) { + CovExp predExp = insnToCovExp.get(predecessor); + if (predExp == null) { + BranchExp branchExp = new BranchExp(exp); + insnToCovExp.put(predecessor, branchExp); + insnToIdx.put(insn, 0); // current insn is the first branch + return true; + } + + BranchExp branchExp = getPredBranchExp(predecessor, insn); + Integer branchIdx = insnToIdx.get(insn); + if (branchIdx == null) { + // Keep track of the instructions in the branches that are already added + branchIdx = branchExp.add(exp); + insnToIdx.put(insn, branchIdx); + } + // If the branch where the instruction is on is already added, no need to do anything as + // branchExp has a reference to exp already. + return false; + } + + /** Finishing the method */ + @Override + public void visitEnd() { + + for (Jump jump : jumps) { + Instruction insn = labelToInsn.get(jump.target); + insn.setPredecessor(jump.source); + predecessors.put(insn, jump.source); + } + + // Compute CovExp for every instruction. + for (Map.Entry<Integer, Instruction> entry : probeToInsn.entrySet()) { + int probeId = entry.getKey(); + Instruction ins = entry.getValue(); + + Instruction insn = ins; + CovExp exp = new ProbeExp(probeId); + + // Compute CovExp for the probed instruction. + CovExp existingExp = insnToCovExp.get(insn); + if (existingExp != null) { + // The instruction already has a branch, add the probeExp as + // a new branch. + if (existingExp instanceof BranchExp) { + BranchExp branchExp = (BranchExp) existingExp; + branchExp.add(exp); + } else { + // This can only happen if the internal data is inconsistent. + // The instruction is a predecessor of another instruction and also + // has a probe, but the branch count is not > 1. + } + } else { + if (insn.getBranches() > 1) { + exp = new BranchExp(exp); + } + insnToCovExp.put(insn, exp); + } + + Instruction predecessor = predecessors.get(insn); + while (predecessor != null) { + if (predecessor.getBranches() > 1) { + boolean isNewBranch = updateBranchPredecessor(predecessor, insn, exp); + if (!isNewBranch) { + // If the branch already exists, no need to visit predecessors any more. + break; + } + } else { + // No branch at predecessor, use the same CovExp + insnToCovExp.put(predecessor, exp); + } + insn = predecessor; + exp = insnToCovExp.get(predecessor); + predecessor = predecessors.get(insn); + } + } + + // Merge branches in the instructions on the same line + for (Instruction insn : instructions) { + if (insn.getBranches() > 1) { + CovExp insnExp = insnToCovExp.get(insn); + if (insnExp != null && (insnExp instanceof BranchExp)) { + BranchExp exp = (BranchExp) insnExp; + BranchExp lineExp = lineToBranchExp.get(insn.getLine()); + if (lineExp == null) { + lineToBranchExp.put(insn.getLine(), exp); + } else { + lineExp.merge(exp); + } + } else { + // If we reach here, the internal data of the mapping is inconsistent, either + // 1) An instruction has branches but we do not create BranchExp for it. + // 2) An instruction has branches but it does not have an associated CovExp. + } + } + } + } + + /** Jumps between instructions and labels */ + class Jump { + public final Instruction source; + public final Label target; + + public Jump(Instruction i, Label l) { + source = i; + target = l; + } + } +} diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/ProbeExp.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/ProbeExp.java new file mode 100644 index 0000000000..888b6c7a55 --- /dev/null +++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/ProbeExp.java @@ -0,0 +1,29 @@ +// 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; + +/** The branch coverage can be evaluated by looking up single probe value. */ +public class ProbeExp implements CovExp { + private final int probeId; + + public ProbeExp(int id) { + probeId = id; + } + + @Override + public boolean eval(final boolean[] probes) { + return probes[probeId]; + } +} |