8 branches for try with resources - jacoco coverage possible?
Well I can't tell you what the exact problem with Jacoco is, but I can show you how Try With Resources is compiled. Basically, there are a lot of compiler generated switches to handle exceptions thrown at various points.
If we take the following code and compile it
public static void main(String[] args){ String a = "before"; try (CharArrayWriter br = new CharArrayWriter()) { br.writeTo(null); } catch (IOException e){ System.out.println(e.getMessage()); } String a2 = "after";}
And then disassemble, we get
.method static public main : ([Ljava/lang/String;)V .limit stack 2 .limit locals 7 .catch java/lang/Throwable from L26 to L30 using L33 .catch java/lang/Throwable from L13 to L18 using L51 .catch [0] from L13 to L18 using L59 .catch java/lang/Throwable from L69 to L73 using L76 .catch [0] from L51 to L61 using L59 .catch java/io/IOException from L3 to L94 using L97 ldc 'before' astore_1L3: new java/io/CharArrayWriter dup invokespecial java/io/CharArrayWriter <init> ()V astore_2 aconst_null astore_3L13: aload_2 aconst_null invokevirtual java/io/CharArrayWriter writeTo (Ljava/io/Writer;)VL18: aload_2 ifnull L94 aload_3 ifnull L44L26: aload_2 invokevirtual java/io/CharArrayWriter close ()VL30: goto L94L33:.stack full locals Object [Ljava/lang/String; Object java/lang/String Object java/io/CharArrayWriter Object java/lang/Throwable stack Object java/lang/Throwable.end stack astore 4 aload_3 aload 4 invokevirtual java/lang/Throwable addSuppressed (Ljava/lang/Throwable;)V goto L94L44:.stack same aload_2 invokevirtual java/io/CharArrayWriter close ()V goto L94L51:.stack same_locals_1_stack_item stack Object java/lang/Throwable.end stack astore 4 aload 4 astore_3 aload 4 athrowL59:.stack same_locals_1_stack_item stack Object java/lang/Throwable.end stack astore 5L61: aload_2 ifnull L91 aload_3 ifnull L87L69: aload_2 invokevirtual java/io/CharArrayWriter close ()VL73: goto L91L76:.stack full locals Object [Ljava/lang/String; Object java/lang/String Object java/io/CharArrayWriter Object java/lang/Throwable Top Object java/lang/Throwable stack Object java/lang/Throwable.end stack astore 6 aload_3 aload 6 invokevirtual java/lang/Throwable addSuppressed (Ljava/lang/Throwable;)V goto L91L87:.stack same aload_2 invokevirtual java/io/CharArrayWriter close ()VL91:.stack same aload 5 athrowL94:.stack full locals Object [Ljava/lang/String; Object java/lang/String stack .end stack goto L108L97:.stack same_locals_1_stack_item stack Object java/io/IOException.end stack astore_2 getstatic java/lang/System out Ljava/io/PrintStream; aload_2 invokevirtual java/io/IOException getMessage ()Ljava/lang/String; invokevirtual java/io/PrintStream println (Ljava/lang/String;)VL108:.stack same ldc 'after' astore_2 return.end method
For those who don't speak bytecode, this is roughly equivalent to the following pseudo Java. I had to use gotos because the bytecode doesn't really correspond to Java control flow.
As you can see, there are a lot of cases to handle the various possibilities of suppressed exceptions. It's not reasonable to be able to cover all these cases. In fact, the goto L59
branch on the first try block is impossible to reach, since the first catch Throwable will catch all exceptions.
try{ CharArrayWriter br = new CharArrayWriter(); Throwable x = null; try{ br.writeTo(null); } catch (Throwable t) {goto L51;} catch (Throwable t) {goto L59;} if (br != null) { if (x != null) { try{ br.close(); } catch (Throwable t) { x.addSuppressed(t); } } else {br.close();} } break; try{ L51: x = t; throw t; L59: Throwable t2 = t; } catch (Throwable t) {goto L59;} if (br != null) { if (x != null) { try{ br.close(); } catch (Throwable t){ x.addSuppressed(t); } } else {br.close();} } throw t2;} catch (IOException e) { System.out.println(e)}
I can cover all 8 branches, so my answer is YES. Look at the following code, this is only a fast try, but it works (or see my github: https://github.com/bachoreczm/basicjava and the 'trywithresources' package, there you can find, how try-with-resources works, see 'ExplanationOfTryWithResources' class):
import java.io.ByteArrayInputStream;import java.io.IOException;import org.junit.Test;public class TestAutoClosable { private boolean isIsNull = false; private boolean logicThrowsEx = false; private boolean closeThrowsEx = false; private boolean getIsThrowsEx = false; private void autoClose() throws Throwable { try (AutoCloseable is = getIs()) { doSomething(); } catch (Throwable t) { System.err.println(t); } } @Test public void test() throws Throwable { try { getIsThrowsEx = true; autoClose(); } catch (Throwable ex) { getIsThrowsEx = false; } } @Test public void everythingOk() throws Throwable { autoClose(); } @Test public void logicThrowsException() { try { logicThrowsEx = true; everythingOk(); } catch (Throwable ex) { logicThrowsEx = false; } } @Test public void isIsNull() throws Throwable { isIsNull = true; everythingOk(); isIsNull = false; } @Test public void closeThrow() { try { closeThrowsEx = true; logicThrowsEx = true; everythingOk(); closeThrowsEx = false; } catch (Throwable ex) { } } @Test public void test2() throws Throwable { try { isIsNull = true; logicThrowsEx = true; everythingOk(); } catch (Throwable ex) { isIsNull = false; logicThrowsEx = false; } } private void doSomething() throws IOException { if (logicThrowsEx) { throw new IOException(); } } private AutoCloseable getIs() throws IOException { if (getIsThrowsEx) { throw new IOException(); } if (closeThrowsEx) { return new ByteArrayInputStream("".getBytes()) { @Override public void close() throws IOException { throw new IOException(); } }; } if (!isIsNull) { return new ByteArrayInputStream("".getBytes()); } return null; }}
No real question, but wanted to throw more research out there. tl;dr = It looks like you can achieve 100% coverage for try-finally, but not for try-with-resource.
Understandably, there's a difference between old-school try-finally and Java7 try-with-resources. Here's two equivalent examples showing the same thing using alternate approaches.
Old School example (a try-finally approach):
final Statement stmt = conn.createStatement();try { foo(); if (stmt != null) { stmt.execute("SELECT 1"); }} finally { if (stmt != null) stmt.close();}
Java7 example (a try-with-resource approach):
try (final Statement stmt = conn.createStatement()) { foo(); if (stmt != null) { stmt.execute("SELECT 1"); }}
Analysis: old-school example:
Using Jacoco 0.7.4.201502262128 and JDK 1.8.0_45, I was able to get 100% line, instruction and branch coverage on the Old School example using the following 4 tests:
- Basic grease path (statement not null, and execute() is exercised normally)
- execute() throws exception
- foo() throws exception AND statement returned as null
- statement returned as null
Analysis: java-7 example:
If the same 4 tests run against the Java7 style example, jacoco indicates 6/8 branches are covered (on the try itself) and 2/2 on the null-check within the try. I tried a number of additional tests to increase coverage, but I can find no way to get better than 6/8. As others have indicated, the decompiled code (which I did also look at) for the java-7 example suggests that the java compiler is generating unreachable segments for try-with-resource. Jacoco is reporting (accurately) that such segments exist.
Update: Using Java7 coding style, you might be able to get 100% coverage IF using a Java7 JRE (see Matyas response below). However, using Java7 coding style with a Java8 JRE, I believe you will hit the 6/8 branches covered. Same code, just different JRE. Seems like the byte code is being created differently between the two JREs with the Java8 one creating unreachable pathways.