diff options
9 files changed, 1092 insertions, 59 deletions
diff --git a/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java b/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java index 6c9db09c..7517fb55 100644 --- a/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java +++ b/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java @@ -44,7 +44,7 @@ public class TryWithResourcesTest extends ValidationTestBase { // without filter next line has branches: assertLine("test.close", ICounter.EMPTY); assertLine("test.catch", ICounter.NOT_COVERED); - assertLine("test.finally", ICounter.PARTLY_COVERED); + assertLine("test.finally", ICounter.FULLY_COVERED); } /** @@ -62,7 +62,7 @@ public class TryWithResourcesTest extends ValidationTestBase { // without filter next line has branches: assertLine("test2.close", ICounter.EMPTY); assertLine("test2.catch", ICounter.NOT_COVERED); - assertLine("test2.finally", ICounter.PARTLY_COVERED); + assertLine("test2.finally", ICounter.FULLY_COVERED); assertLine("test2.after", ICounter.FULLY_COVERED); } @@ -109,14 +109,14 @@ public class TryWithResourcesTest extends ValidationTestBase { assertLine("nested.try2", ICounter.FULLY_COVERED); assertLine("nested.body", ICounter.FULLY_COVERED); assertLine("nested.catch2", ICounter.NOT_COVERED); - assertLine("nested.finally2", ICounter.PARTLY_COVERED); + assertLine("nested.finally2", ICounter.FULLY_COVERED); // next lines not covered on exceptional path: - assertLine("nested.try3", ICounter.PARTLY_COVERED, 0, 0); - assertLine("nested.open3", ICounter.PARTLY_COVERED, 0, 0); - assertLine("nested.body3", ICounter.PARTLY_COVERED, 0, 0); + assertLine("nested.try3", ICounter.FULLY_COVERED, 0, 0); + assertLine("nested.open3", ICounter.FULLY_COVERED, 0, 0); + assertLine("nested.body3", ICounter.FULLY_COVERED, 0, 0); assertLine("nested.catch3", ICounter.NOT_COVERED); - assertLine("nested.finally3", ICounter.PARTLY_COVERED, 0, 0); + assertLine("nested.finally3", ICounter.FULLY_COVERED, 0, 0); // without filter next lines have branches: assertLine("nested.close3", ICounter.EMPTY); @@ -132,12 +132,12 @@ public class TryWithResourcesTest extends ValidationTestBase { // without filter next line covered partly: assertLine("returnInCatch.try1", ICounter.FULLY_COVERED); assertLine("returnInCatch.open", ICounter.FULLY_COVERED); - assertLine("returnInCatch.finally1", ICounter.PARTLY_COVERED, 5, 1); + assertLine("returnInCatch.finally1", ICounter.PARTLY_COVERED, 1, 1); // without filter next line has branches: assertLine("returnInCatch.close", ICounter.EMPTY); assertLine("returnInCatch.try2", ICounter.EMPTY); - assertLine("returnInCatch.finally2", ICounter.PARTLY_COVERED, 5, 1); + assertLine("returnInCatch.finally2", ICounter.PARTLY_COVERED, 1, 1); } /* diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FinallyFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FinallyFilterTest.java new file mode 100644 index 00000000..64ad884d --- /dev/null +++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FinallyFilterTest.java @@ -0,0 +1,421 @@ +/******************************************************************************* + * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Evgeny Mandrikov - initial API and implementation + * + *******************************************************************************/ +package org.jacoco.core.internal.analysis.filter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.HashSet; +import java.util.Set; + +import org.jacoco.core.internal.instr.InstrSupport; +import org.junit.Test; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodNode; + +public class FinallyFilterTest implements IFilterOutput { + + private final IFilter filter = new FinallyFilter(); + + private final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "name", "()V", null, null); + + /** + * <pre> + * try { + * ... + * if (...) { + * ... + * return; + * } else { + * ... + * return; + * } + * } finally { + * ... + * } + * </pre> + */ + @Test + public void should_analyze_control_flow() { + final Label start1 = new Label(); + final Label end1 = new Label(); + final Label start2 = new Label(); + final Label end2 = new Label(); + final Label finallyStart = new Label(); + + m.visitTryCatchBlock(start1, end1, finallyStart, null); + m.visitTryCatchBlock(start2, end2, finallyStart, null); + + m.visitLabel(start1); + // jump to another region associated with same handler: + m.visitJumpInsn(Opcodes.IFEQ, start2); + m.visitInsn(Opcodes.NOP); + m.visitLabel(end1); + + m.visitInsn(Opcodes.NOP); // finally block + shouldMergeLast(); + m.visitInsn(Opcodes.RETURN); + + m.visitLabel(start2); + m.visitInsn(Opcodes.NOP); + m.visitLabel(end2); + m.visitInsn(Opcodes.NOP); // finally block + shouldMergeLast(); + m.visitInsn(Opcodes.RETURN); + + m.visitLabel(finallyStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.NOP); // finally block + shouldMergeLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.ATHROW); + shouldIgnoreLast(); + + execute(); + } + + // === try/catch/finally === + + @Test + public void javac_try_catch_finally() { + final Label tryStart = new Label(); + final Label tryEnd = new Label(); + final Label catchStart = new Label(); + final Label catchEnd = new Label(); + final Label finallyStart = new Label(); + final Label finallyEnd = new Label(); + + m.visitTryCatchBlock(tryStart, tryEnd, catchStart, + "java/lang/Exception"); + m.visitTryCatchBlock(catchStart, catchEnd, finallyStart, null); + m.visitTryCatchBlock(tryStart, tryEnd, finallyStart, null); + + m.visitLabel(tryStart); + m.visitInsn(Opcodes.NOP); // try body + m.visitLabel(tryEnd); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitJumpInsn(Opcodes.GOTO, finallyEnd); + shouldIgnoreLast(); + + m.visitLabel(catchStart); + m.visitInsn(Opcodes.NOP); // catch body + m.visitLabel(catchEnd); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitInsn(Opcodes.ATHROW); + + m.visitLabel(finallyStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.ATHROW); + shouldIgnoreLast(); + m.visitLabel(finallyEnd); + + m.visitInsn(Opcodes.NOP); + + execute(); + } + + @Test + public void ecj_try_catch_finally() { + final Label tryStart = new Label(); + final Label tryEnd = new Label(); + final Label catchStart = new Label(); + final Label catchEnd = new Label(); + final Label finallyStart = new Label(); + final Label finallyEnd = new Label(); + final Label after = new Label(); + + m.visitTryCatchBlock(tryStart, tryEnd, catchStart, + "java/lang/Exception"); + m.visitTryCatchBlock(tryStart, catchEnd, finallyStart, null); + + m.visitLabel(tryStart); + m.visitInsn(Opcodes.NOP); // try body + m.visitLabel(tryEnd); + m.visitJumpInsn(Opcodes.GOTO, finallyEnd); + + m.visitLabel(catchStart); + m.visitInsn(Opcodes.POP); + m.visitInsn(Opcodes.NOP); // catch body + m.visitLabel(catchEnd); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitJumpInsn(Opcodes.GOTO, after); + shouldIgnoreLast(); + + m.visitLabel(finallyStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.ATHROW); + shouldIgnoreLast(); + m.visitLabel(finallyEnd); + + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitLabel(after); + m.visitInsn(Opcodes.NOP); + + execute(); + } + + // === empty catch === + + /** + * javac 1.5 - 1.7 + */ + @Test + public void javac_empty_catch() { + final Label tryStart = new Label(); + final Label tryEnd = new Label(); + final Label catchStart = new Label(); + final Label catchEnd = new Label(); + final Label finallyStart = new Label(); + final Label finallyEnd = new Label(); + + m.visitTryCatchBlock(tryStart, tryEnd, catchStart, + "java/lang/Exception"); + m.visitTryCatchBlock(tryStart, tryEnd, finallyStart, null); + m.visitTryCatchBlock(catchStart, catchEnd, finallyStart, null); + // actually one more useless TryCatchBlock for ASTORE in finally + + m.visitLabel(tryStart); + m.visitInsn(Opcodes.NOP); // try body + m.visitLabel(tryEnd); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitJumpInsn(Opcodes.GOTO, finallyEnd); + shouldIgnoreLast(); + + m.visitLabel(catchStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + m.visitLabel(catchEnd); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitJumpInsn(Opcodes.GOTO, finallyEnd); + shouldIgnoreLast(); + + m.visitLabel(finallyStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.ATHROW); + shouldIgnoreLast(); + m.visitLabel(finallyEnd); + + m.visitInsn(Opcodes.NOP); + + execute(); + } + + /** + * javac >= 1.8 + * + * Probably related to https://bugs.openjdk.java.net/browse/JDK-7093325 + */ + @Test + public void javac_8_empty_catch() throws Exception { + final Label tryStart = new Label(); + final Label tryEnd = new Label(); + final Label catchStart = new Label(); + final Label finallyStart = new Label(); + final Label finallyEnd = new Label(); + + m.visitTryCatchBlock(tryStart, tryEnd, catchStart, + "java/lang/Exception"); + m.visitTryCatchBlock(tryStart, tryEnd, finallyStart, null); + + m.visitLabel(tryStart); + m.visitInsn(Opcodes.NOP); // try body + m.visitLabel(tryEnd); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitJumpInsn(Opcodes.GOTO, finallyEnd); + shouldIgnoreLast(); + shouldIgnoreLast(); + + m.visitLabel(catchStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitJumpInsn(Opcodes.GOTO, finallyEnd); + shouldIgnoreLast(); + + m.visitLabel(finallyStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.ATHROW); + shouldIgnoreLast(); + m.visitLabel(finallyEnd); + + execute(); + } + + @Test + public void ecj_empty_catch() { + final Label tryStart = new Label(); + final Label tryEnd = new Label(); + final Label catchStart = new Label(); + final Label catchEnd = new Label(); + final Label finallyStart = new Label(); + final Label finallyEnd = new Label(); + final Label after = new Label(); + + m.visitTryCatchBlock(tryStart, tryEnd, catchStart, + "java/lang/Exception"); + m.visitTryCatchBlock(tryStart, catchEnd, finallyStart, null); + + m.visitLabel(tryStart); + m.visitInsn(Opcodes.NOP); // try body + m.visitLabel(tryEnd); + m.visitJumpInsn(Opcodes.GOTO, finallyEnd); + + m.visitLabel(catchStart); + m.visitInsn(Opcodes.POP); + m.visitLabel(catchEnd); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitJumpInsn(Opcodes.GOTO, after); + shouldIgnoreLast(); + + m.visitLabel(finallyStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + shouldIgnoreLast(); + m.visitInsn(Opcodes.ATHROW); + shouldIgnoreLast(); + m.visitLabel(finallyEnd); + + m.visitInsn(Opcodes.NOP); // finally body + shouldMergeLast(); + m.visitLabel(after); + m.visitInsn(Opcodes.NOP); + + execute(); + } + + // === always completes abruptly === + + @Test + public void javac_always_completes_abruptly() { + final Label tryStart = new Label(); + final Label tryEnd = new Label(); + final Label finallyStart = new Label(); + + m.visitTryCatchBlock(tryStart, tryEnd, finallyStart, null); + + m.visitLabel(tryStart); + m.visitInsn(Opcodes.NOP); // try body + m.visitLabel(tryEnd); + m.visitInsn(Opcodes.RETURN); // finally body + + m.visitLabel(finallyStart); + m.visitVarInsn(Opcodes.ASTORE, 1); + m.visitInsn(Opcodes.RETURN); // finally body + + execute(); + } + + @Test + public void ecj_always_completes_abruptly() { + final Label tryStart = new Label(); + final Label tryEnd = new Label(); + final Label finallyStart = new Label(); + + m.visitTryCatchBlock(tryStart, tryEnd, tryEnd, null); + + m.visitLabel(tryStart); + m.visitInsn(Opcodes.NOP); // try body + m.visitJumpInsn(Opcodes.GOTO, finallyStart); + m.visitLabel(tryEnd); + + m.visitInsn(Opcodes.POP); + m.visitLabel(finallyStart); + m.visitInsn(Opcodes.RETURN); // finally body + + execute(); + } + + private final Set<AbstractInsnNode> expectedIgnored = new HashSet<AbstractInsnNode>(); + private final Set<AbstractInsnNode> actualIgnored = new HashSet<AbstractInsnNode>(); + private final Set<AbstractInsnNode> expectedMerged = new HashSet<AbstractInsnNode>(); + private final Set<AbstractInsnNode> actualMerged = new HashSet<AbstractInsnNode>(); + + private void shouldMergeLast() { + expectedMerged.add(m.instructions.getLast()); + } + + private void shouldIgnoreLast() { + expectedIgnored.add(m.instructions.getLast()); + } + + private void execute() { + filter.filter("", "", m, this); + assertEquals("ignored", toIndexes(expectedIgnored), + toIndexes(actualIgnored)); + assertEquals("merged", toIndexes(expectedMerged), + toIndexes(actualMerged)); + } + + @SuppressWarnings("boxing") + private Set<Integer> toIndexes(Set<AbstractInsnNode> set) { + final Set<Integer> result = new HashSet<Integer>(); + for (final AbstractInsnNode i : set) { + result.add(m.instructions.indexOf(i)); + } + return result; + } + + public void ignore(final AbstractInsnNode fromInclusive, + final AbstractInsnNode toInclusive) { + for (AbstractInsnNode i = fromInclusive; i != toInclusive; i = i + .getNext()) { + actualIgnored.add(i); + } + actualIgnored.add(toInclusive); + } + + public void merge(final AbstractInsnNode i1, final AbstractInsnNode i2) { + if (actualMerged.isEmpty() || actualMerged.contains(i1) + || actualMerged.contains(i2)) { + actualMerged.add(i1); + actualMerged.add(i2); + } else { + fail(); + } + } + +} diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/filter/FinallyTest.java b/org.jacoco.core.test/src/org/jacoco/core/test/filter/FinallyTest.java new file mode 100644 index 00000000..7658a118 --- /dev/null +++ b/org.jacoco.core.test/src/org/jacoco/core/test/filter/FinallyTest.java @@ -0,0 +1,266 @@ +/******************************************************************************* + * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Evgeny Mandrikov - initial API and implementation + * + *******************************************************************************/ +package org.jacoco.core.test.filter; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import org.jacoco.core.analysis.ICounter; +import org.jacoco.core.test.TargetLoader; +import org.jacoco.core.test.filter.targets.Finally; +import org.jacoco.core.test.validation.Source; +import org.jacoco.core.test.validation.ValidationTestBase; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.LineNumberNode; +import org.objectweb.asm.tree.MethodNode; + +/** + * Test of filtering of duplicated bytecode that is generated for finally block. + */ +public class FinallyTest extends ValidationTestBase { + + private static boolean isJDK8 = !Pattern.compile("1\\.[567]\\.0_(\\d++)") + .matcher(System.getProperty("java.version")).matches(); + + public FinallyTest() { + super(Finally.class); + } + + /** + * {@link Finally#example(boolean)} + */ + @Test + public void example() { + if (isJDKCompiler) { + assertLine("example.0", ICounter.EMPTY); + } else { + assertLine("example.0", ICounter.FULLY_COVERED); + } + assertLine("example.1", ICounter.FULLY_COVERED, 0, 2); + assertLine("example.2", ICounter.FULLY_COVERED); + assertLine("example.3", ICounter.EMPTY); + assertLine("example.4", ICounter.EMPTY); + } + + /** + * GOTO instructions at the end of duplicates of finally block might have + * line number of a last instruction of finally block and hence lead to + * unexpected coverage results, like for example in case of ECJ for + * {@link Finally#catchNotExecuted()}, {@link Finally#emptyCatch()}. So we + * decided to ignore them, even if they can correspond to a real break + * statement. + * <p> + * See also <a href= + * "https://bugs.openjdk.java.net/browse/JDK-8180141">JDK-8180141</a> and + * <a href= + * "https://bugs.openjdk.java.net/browse/JDK-7008643">JDK-7008643</a>. + * <p> + * {@link Finally#breakStatement()} + */ + @Test + public void breakStatement() { + assertLine("breakStatement", ICounter.EMPTY); + + assertLine("breakStatement.1", ICounter.FULLY_COVERED); + assertLine("breakStatement.2", ICounter.EMPTY); + } + + /** + * {@link Finally#catchNotExecuted()} + */ + @Test + public void catchNotExecuted() { + assertLine("catchNotExecuted.catch", ICounter.NOT_COVERED); + assertLine("catchNotExecuted.0", ICounter.EMPTY); + assertLine("catchNotExecuted.1", ICounter.FULLY_COVERED); + assertLine("catchNotExecuted.2", ICounter.EMPTY); + } + + /** + * {@link Finally#emptyCatch()} + */ + @Test + public void emptyCatch() { + assertLine("emptyCatch.0", ICounter.EMPTY); + assertLine("emptyCatch.1", ICounter.FULLY_COVERED); + assertLine("emptyCatch.2", ICounter.EMPTY); + } + + /** + * {@link Finally#twoRegions()} + */ + @Test + public void twoRegions() { + assertLine("twoRegions.0", ICounter.EMPTY); + if (isJDKCompiler && !isJDK8) { + // https://bugs.openjdk.java.net/browse/JDK-7008643 + assertLine("twoRegions.1", ICounter.PARTLY_COVERED); + assertLine("twoRegions.return.1", ICounter.EMPTY); + assertLine("twoRegions.return.2", ICounter.EMPTY); + } else { + assertLine("twoRegions.1", ICounter.FULLY_COVERED); + assertLine("twoRegions.return.1", ICounter.FULLY_COVERED); + assertLine("twoRegions.return.2", ICounter.NOT_COVERED); + } + assertLine("twoRegions.2", ICounter.EMPTY); + + assertLine("twoRegions.if", ICounter.FULLY_COVERED); + assertLine("twoRegions.region.1", ICounter.FULLY_COVERED); + assertLine("twoRegions.region.2", ICounter.NOT_COVERED); + } + + /** + * {@link Finally#nested()} + */ + @Test + public void nested() { + if (isJDKCompiler) { + assertLine("nested.0", ICounter.EMPTY); + } else { + assertLine("nested.0", ICounter.FULLY_COVERED); + } + assertLine("nested.1", ICounter.EMPTY); + assertLine("nested.2", ICounter.FULLY_COVERED); + if (isJDKCompiler) { + assertLine("nested.3", ICounter.EMPTY); + } else { + assertLine("nested.3", ICounter.FULLY_COVERED); + } + assertLine("nested.4", ICounter.FULLY_COVERED); + } + + /** + * {@link Finally#emptyTry()} + */ + @Test + public void emptyTry() { + assertLine("emptyTry.0", ICounter.EMPTY); + if (!isJDKCompiler || isJDK8) { + assertLine("emptyTry.1", ICounter.FULLY_COVERED); + assertLine("emptyTry.2", ICounter.EMPTY); + } else { + // compiler bug fixed in javac >= 1.8: + assertLine("emptyTry.1", ICounter.PARTLY_COVERED); + assertLine("emptyTry.2", ICounter.FULLY_COVERED); + } + } + + /** + * {@link Finally#alwaysCompletesAbruptly()} + */ + @Test + public void alwaysCompletesAbruptly() { + if (isJDKCompiler) { + // uncovered case: + assertLine("alwaysCompletesAbruptly.0", ICounter.EMPTY); + assertLine("alwaysCompletesAbruptly.1", ICounter.PARTLY_COVERED); + } else { + assertLine("alwaysCompletesAbruptly.0", ICounter.PARTLY_COVERED); + assertLine("alwaysCompletesAbruptly.1", ICounter.FULLY_COVERED); + } + assertLine("alwaysCompletesAbruptly.2", ICounter.EMPTY); + } + + /** + * This test studies placement of GOTO instructions. + */ + @Test + public void gotos() throws IOException { + final Source source = Source.getSourceFor("src", Finally.class); + + final ClassNode classNode = new ClassNode(); + new ClassReader(TargetLoader.getClassDataAsBytes(Finally.class)) + .accept(classNode, 0); + final Set<String> tags = new HashSet<String>(); + for (final MethodNode m : classNode.methods) { + if ("main".equals(m.name)) { + // skip it + continue; + } + int lineNumber = -1; + for (AbstractInsnNode i = m.instructions + .getFirst(); i != null; i = i.getNext()) { + if (AbstractInsnNode.LINE == i.getType()) { + lineNumber = ((LineNumberNode) i).line; + } + if (Opcodes.GOTO == i.getOpcode()) { + final String line = source.getLine(lineNumber); + if (line.indexOf('$') < 0) { + throw new AssertionError( + "No tag at line " + lineNumber); + } + final String tag = line.substring( + line.indexOf('$') + "$line-".length(), + line.lastIndexOf('$')); + tags.add(tag); + } + } + } + + final Set<String> expected = new HashSet<String>(); + + if (isJDKCompiler) { + expected.add("example.2"); + } else { + expected.add("example.0"); + } + + expected.add("breakStatement.for"); + if (isJDKCompiler) { + expected.add("breakStatement.1"); + expected.add("breakStatement.2"); + } else { + expected.add("breakStatement"); + } + + if (isJDKCompiler) { + expected.add("emptyCatch.2"); + } else { + expected.add("emptyCatch"); + expected.add("emptyCatch.1"); + } + + if (isJDKCompiler) { + expected.add("catchNotExecuted.2"); + } else { + expected.add("catchNotExecuted"); + expected.add("catchNotExecuted.1"); + } + + if (isJDKCompiler) { + expected.add("nested.5"); + expected.add("nested.6"); + } else { + expected.add("nested.0"); + expected.add("nested.3"); + } + + if (isJDKCompiler && !isJDK8) { + expected.add("emptyTry.2"); + } + + if (!isJDKCompiler) { + expected.add("alwaysCompletesAbruptly.0"); + } + + assertEquals(expected, tags); + } + +} diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/Finally.java b/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/Finally.java new file mode 100644 index 00000000..04950e91 --- /dev/null +++ b/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/Finally.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Evgeny Mandrikov - initial API and implementation + * + *******************************************************************************/ +package org.jacoco.core.test.filter.targets; + +import static org.jacoco.core.test.validation.targets.Stubs.ex; +import static org.jacoco.core.test.validation.targets.Stubs.f; +import static org.jacoco.core.test.validation.targets.Stubs.nop; +import static org.jacoco.core.test.validation.targets.Stubs.t; + +public class Finally { + + /** + * <pre> + * InputStream in = null; + * try { + * in = ...; + * ... + * } finally { + * if (in != null) { + * in.close(); + * } + * } + * </pre> + */ + private static void example(boolean t) { + Object in = null; + try { + in = open(t); + } finally { // $line-example.0$ + if (in != null) { // $line-example.1$ + nop(); // $line-example.2$ + } // $line-example.3$ + } // $line-example.4$ + } + + private static Object open(boolean t) { + ex(t); + return new Object(); + } + + private static void breakStatement() { + for (int i = 0; i < 1; i++) { // $line-breakStatement.for$ + try { + if (f()) { + break; // $line-breakStatement$ + } + } finally { + nop("finally"); // $line-breakStatement.1$ + } // $line-breakStatement.2$ + } + } + + private static void catchNotExecuted() { + try { + nop("try"); + } catch (Exception e) { // $line-catchNotExecuted$ + nop("catch"); // $line-catchNotExecuted.catch$ + } finally { // $line-catchNotExecuted.0$ + nop("finally"); // $line-catchNotExecuted.1$ + } // $line-catchNotExecuted.2$ + } + + private static void emptyCatch() { + try { + nop("try"); + } catch (Exception e) { // $line-emptyCatch$ + // empty + } finally { // $line-emptyCatch.0$ + nop("finally"); // $line-emptyCatch.1$ + } // $line-emptyCatch.2$ + } + + private static void twoRegions() { + try { + // jump to another region associated with same handler: + if (t()) { // $line-twoRegions.if$ + nop(); // $line-twoRegions.region.1$ + return; // $line-twoRegions.return.1$ + } else { + nop(); // $line-twoRegions.region.2$ + return; // $line-twoRegions.return.2$ + } + } finally { // $line-twoRegions.0$ + nop(); // $line-twoRegions.1$ + } // $line-twoRegions.2$ + } + + private static void nested() { + try { + nop(); + } finally { // $line-nested.0$ + try { // $line-nested.1$ + nop(); // $line-nested.2$ + } finally { // $line-nested.3$ + nop(); // $line-nested.4$ + } // $line-nested.5$ + } // $line-nested.6$ + } + + private static void emptyTry() { + try { + // empty + } finally { // $line-emptyTry.0$ + nop(); // $line-emptyTry.1$ + } // $line-emptyTry.2$ + } + + @SuppressWarnings("finally") + private static void alwaysCompletesAbruptly() { + try { + nop(); + } finally { // $line-alwaysCompletesAbruptly.0$ + return; // $line-alwaysCompletesAbruptly.1$ + } // $line-alwaysCompletesAbruptly.2$ + } + + public static void main(String[] args) { + example(false); + try { + example(true); + } catch (Exception ignore) { + } + + breakStatement(); + + catchNotExecuted(); + + emptyCatch(); + + twoRegions(); + + nested(); + + emptyTry(); + + alwaysCompletesAbruptly(); + } + +} diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java index e1baec7d..3ec77f33 100644 --- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java +++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java @@ -15,27 +15,11 @@ import org.jacoco.core.analysis.ICounter; import org.jacoco.core.test.validation.targets.Target03; import org.junit.Test; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * Tests of exception based control flow. */ public class ExceptionsTest extends ValidationTestBase { - /** - * https://bugs.openjdk.java.net/browse/JDK-8180660 - */ - private static final boolean isJDK8u152; - private static final boolean isJDK10; - - static { - final Matcher m = Pattern.compile("1\\.8\\.0_(\\d++)(-ea)?") - .matcher(System.getProperty("java.version")); - isJDK8u152 = m.matches() && Integer.parseInt(m.group(1)) >= 152; - isJDK10 = System.getProperty("java.version").startsWith("10"); - } - public ExceptionsTest() { super(Target03.class); } @@ -109,45 +93,25 @@ public class ExceptionsTest extends ValidationTestBase { ICounter.FULLY_COVERED); // 7. Finally Block Without Exception Thrown - // Finally block is yellow as the exception path is missing. assertLine("noExceptionFinally.beforeBlock", ICounter.FULLY_COVERED); assertLine("noExceptionFinally.tryBlock", ICounter.FULLY_COVERED); assertLine("noExceptionFinally.finally", - isJDKCompiler ? ICounter.EMPTY : ICounter.PARTLY_COVERED); - assertLine("noExceptionFinally.finallyBlock", ICounter.PARTLY_COVERED); - if (!isJDKCompiler) { - assertLine("noExceptionFinally.finallyBlockEnd", - ICounter.NOT_COVERED); - } else if (isJDK8u152 || isJDK10) { - assertLine("noExceptionFinally.finallyBlockEnd", - ICounter.PARTLY_COVERED); - } else { - assertLine("noExceptionFinally.finallyBlockEnd", - ICounter.FULLY_COVERED); - } + isJDKCompiler ? ICounter.EMPTY : ICounter.FULLY_COVERED); + assertLine("noExceptionFinally.finallyBlock", ICounter.FULLY_COVERED); + assertLine("noExceptionFinally.finallyBlockEnd", ICounter.EMPTY); assertLine("noExceptionFinally.afterBlock", ICounter.FULLY_COVERED); // 8. Finally Block With Implicit Exception - // Finally block is yellow as the non-exception path is missing. assertLine("implicitExceptionFinally.beforeBlock", ICounter.FULLY_COVERED); assertLine("implicitExceptionFinally.before", ICounter.FULLY_COVERED); assertLine("implicitExceptionFinally.exception", ICounter.NOT_COVERED); assertLine("implicitExceptionFinally.after", ICounter.NOT_COVERED); assertLine("implicitExceptionFinally.finally", - isJDKCompiler ? ICounter.EMPTY : ICounter.PARTLY_COVERED); + isJDKCompiler ? ICounter.EMPTY : ICounter.NOT_COVERED); assertLine("implicitExceptionFinally.finallyBlock", - ICounter.PARTLY_COVERED); - if (!isJDKCompiler) { - assertLine("implicitExceptionFinally.finallyBlockEnd", - ICounter.FULLY_COVERED); - } else if (isJDK8u152 || isJDK10) { - assertLine("implicitExceptionFinally.finallyBlockEnd", - ICounter.PARTLY_COVERED); - } else { - assertLine("implicitExceptionFinally.finallyBlockEnd", - ICounter.NOT_COVERED); - } + ICounter.FULLY_COVERED); + assertLine("implicitExceptionFinally.finallyBlockEnd", ICounter.EMPTY); assertLine("implicitExceptionFinally.afterBlock", ICounter.NOT_COVERED); // 9. Finally Block With Exception Thrown Explicitly @@ -159,13 +123,8 @@ public class ExceptionsTest extends ValidationTestBase { isJDKCompiler ? ICounter.EMPTY : ICounter.FULLY_COVERED); assertLine("explicitExceptionFinally.finallyBlock", ICounter.FULLY_COVERED); - if (!isJDKCompiler || isJDK8u152 || isJDK10) { - assertLine("explicitExceptionFinally.finallyBlockEnd", - ICounter.FULLY_COVERED); - } else { - assertLine("explicitExceptionFinally.finallyBlockEnd", - ICounter.EMPTY); - } + assertLine("explicitExceptionFinally.finallyBlockEnd", + isJDKCompiler ? ICounter.EMPTY : ICounter.FULLY_COVERED); assertLine("explicitExceptionFinally.afterBlock", ICounter.EMPTY); } diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java index 97b49e4b..8200fcf7 100644 --- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java +++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java @@ -108,6 +108,15 @@ public class Stubs { } /** + * Throws a {@link RuntimeException} if given argument is <code>true</code>. + */ + public static void ex(boolean t) { + if (t) { + throw new StubException(); + } + } + + /** * Directly executes the given runnable. */ public static void exec(Runnable task) { diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java index 4b2a64a9..8696151a 100644 --- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java @@ -29,7 +29,7 @@ public final class Filters implements IFilter { public static final IFilter ALL = new Filters(new EnumFilter(), new SyntheticFilter(), new SynchronizedFilter(), new TryWithResourcesJavacFilter(), new TryWithResourcesEcjFilter(), - new PrivateEmptyNoArgConstructorFilter(), + new FinallyFilter(), new PrivateEmptyNoArgConstructorFilter(), new StringSwitchJavacFilter(), new LombokGeneratedFilter(), new GroovyGeneratedFilter()); diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/FinallyFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/FinallyFilter.java new file mode 100644 index 00000000..0ab6ca2f --- /dev/null +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/FinallyFilter.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Evgeny Mandrikov - initial API and implementation + * + *******************************************************************************/ +package org.jacoco.core.internal.analysis.filter; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TryCatchBlockNode; +import org.objectweb.asm.tree.VarInsnNode; + +/** + * Filters duplicates of finally blocks that compiler generates. + * + * To understand algorithm of filtering, consider following example: + * + * <pre> + * try { + * if (x) { + * a(); + * return; // 1 + * } + * b(); // 2 + * } catch (Exception e) { + * c(); // 3 + * } finally { + * d(); // 4 + * } + * </pre> + * + * There are 4 <b>distinct</b> points of exit out of these "try/catch/finally" + * blocks - three without exception, and one with Throwable if it is thrown + * prior to reaching first three points of exit. + * + * "finally" block must be executed just before these points, so there must be 4 + * copies of its bytecode instructions. + * + * One of them handles Throwable ("catch-any") and must cover all instructions + * of "try/catch" blocks. But must not cover instructions of other duplicates, + * because instructions of "finally" block also can cause Throwable to be + * thrown. + * + * Therefore there will be multiple {@link MethodNode#tryCatchBlocks} with + * {@link TryCatchBlockNode#type} null with same + * {@link TryCatchBlockNode#handler} for different non-intersecting bytecode + * regions ({@link TryCatchBlockNode#start}, {@link TryCatchBlockNode#end}). + * + * And each exit out of these regions, except one that handles Throwable, will + * contain duplicate of "finally" block. + * + * To determine exits out of these regions, they all must be processed together + * at once, because execution can branch from one region to another (like it is + * in given example due to "if" statement). + */ +public final class FinallyFilter implements IFilter { + + public void filter(final String className, final String superClassName, + final MethodNode methodNode, final IFilterOutput output) { + for (final TryCatchBlockNode tryCatchBlock : methodNode.tryCatchBlocks) { + if (tryCatchBlock.type == null) { + filter(output, methodNode.tryCatchBlocks, tryCatchBlock); + } + } + } + + private static void filter(final IFilterOutput output, + final List<TryCatchBlockNode> tryCatchBlocks, + final TryCatchBlockNode catchAnyBlock) { + final AbstractInsnNode e = next(catchAnyBlock.handler); + final int size = size(e); + if (size <= 0) { + return; + } + + // Determine instructions inside regions + final Set<AbstractInsnNode> inside = new HashSet<AbstractInsnNode>(); + for (final TryCatchBlockNode t : tryCatchBlocks) { + if (t.handler == catchAnyBlock.handler) { + AbstractInsnNode i = t.start; + while (i != t.end) { + inside.add(i); + i = i.getNext(); + } + } + } + + // Find and merge duplicates at exits of regions + for (final TryCatchBlockNode t : tryCatchBlocks) { + if (t.handler == catchAnyBlock.handler) { + boolean continues = false; + AbstractInsnNode i = t.start; + + while (i != t.end) { + switch (i.getType()) { + case AbstractInsnNode.FRAME: + case AbstractInsnNode.LINE: + case AbstractInsnNode.LABEL: + break; + case AbstractInsnNode.JUMP_INSN: + final AbstractInsnNode jumpTarget = next( + ((JumpInsnNode) i).label); + if (!inside.contains(jumpTarget)) { + merge(output, size, e, jumpTarget); + } + continues = i.getOpcode() != Opcodes.GOTO; + break; + default: + switch (i.getOpcode()) { + case Opcodes.IRETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.DRETURN: + case Opcodes.ARETURN: + case Opcodes.RETURN: + case Opcodes.ATHROW: + continues = false; + break; + default: + continues = true; + break; + } + break; + } + i = i.getNext(); + } + + i = next(i); + if (continues && !inside.contains(i)) { + merge(output, size, e, i); + } + } + + if (t != catchAnyBlock && t.start == catchAnyBlock.start + && t.end == catchAnyBlock.end) { + final AbstractInsnNode i = next(next(t.handler)); + if (!inside.contains(i)) { + // javac's empty catch - merge after ASTORE + merge(output, size, e, i); + } + } + } + } + + private static void merge(final IFilterOutput output, final int size, + AbstractInsnNode e, AbstractInsnNode n) { + if (!isSame(size, e, n)) { + return; + } + output.ignore(e, e); + e = next(e); + for (int i = 0; i < size; i++) { + output.merge(e, n); + e = next(e); + n = next(n); + } + output.ignore(e, next(e)); + + if (n != null && n.getOpcode() == Opcodes.GOTO) { + // goto instructions at the end of non-executed duplicates + // cause partial coverage of last line of finally block, + // so should be ignored + output.ignore(n, n); + } + } + + private static boolean isSame(final int size, AbstractInsnNode e, + AbstractInsnNode n) { + e = next(e); + for (int i = 0; i < size; i++) { + if (n == null || e.getOpcode() != n.getOpcode()) { + return false; + } + e = next(e); + n = next(n); + } + return true; + } + + /** + * @return number of instructions inside given "catch-any" handler + */ + private static int size(AbstractInsnNode i) { + if (Opcodes.ASTORE != i.getOpcode()) { + // when always completes abruptly + return 0; + } + final int var = ((VarInsnNode) i).var; + int size = -1; + do { + size++; + i = next(i); + if (i == null) { + // when always completes abruptly + return 0; + } + } while (!(Opcodes.ALOAD == i.getOpcode() + && var == ((VarInsnNode) i).var)); + i = next(i); + if (Opcodes.ATHROW != i.getOpcode()) { + return 0; + } + return size; + } + + private static AbstractInsnNode next(AbstractInsnNode i) { + do { + i = i.getNext(); + } while (i != null && (AbstractInsnNode.FRAME == i.getType() + || AbstractInsnNode.LABEL == i.getType() + || AbstractInsnNode.LINE == i.getType())); + return i; + } + +} diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html index 9035dc36..ee78110f 100644 --- a/org.jacoco.doc/docroot/doc/changes.html +++ b/org.jacoco.doc/docroot/doc/changes.html @@ -34,6 +34,9 @@ <li>Exclude from a report a part of bytecode that compiler generates for a try-with-resources statement (GitHub <a href="https://github.com/jacoco/jacoco/issues/500">#500</a>).</li> + <li>During creation of a report merge duplicate instructions that compiler generates + for finally blocks + (GitHub <a href="https://github.com/jacoco/jacoco/issues/604">#604</a>).</li> <li>Exclude from a report methods which are annotated with <code>@lombok.Generated</code>. Initial analysis and contribution by RĂ¼diger zu Dohna (GitHub <a href="https://github.com/jacoco/jacoco/issues/513">#513</a>).</li> |