I've got some code that uses try with resources and in jacoco it's coming up as only half covered. All the source code lines are green, but I get a little yellow symbol telling me that only 4 of 8 branches are covered.
I'm having trouble figuring out what all the branches are, and how to write code that covers them. Three possible places throw PipelineException
. These are createStageList()
, processItem()
and the implied close()
- Not throwing any exceptions,
- throwing an exception from
createStageList()
- throwing an exception from
processItem()
- throwing an exception from
close()
- throwing an exception from
processItem()
andclose()
I can't think of any other cases, yet I still only have 4 of 8 covered.
Can someone explain to me why it's 4 of 8 and is there anyway to hit all 8 branches? I'm not skilled with decyrpting/reading/interpreting byte code, but maybe you are... :) I've already seen https://github.com/jacoco/jacoco/issues/82, but neither it nor the issue it references help very much (other than noting that this is due to compiler generated blocks)
Hmm, just as I finish writing this I had a thought on what case(s) might not be not tested by what I mention above... I'll post an answer if I got it right. I'm sure this question and it's answer will help someone in any case.
EDIT: Nope, I didn't find it. Throwing RuntimeExceptions (not handled by the catch block) didn't cover any more branches
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):
Four years old, but still...
AutoCloseable
AutoCloseable
try
block butAutoCloseable
is nullAbove lists all 7 conditions - the reason for the 8 branches is due to repeated condition.
All branches can be reached, the
try-with-resources
is fairly simple compiler sugar (at least compared toswitch-on-string
) - if they cannot be reached, then it is by definition a compiler bug.Only 6 unit tests are actually required (in the example code below,
throwsOnClose
is@Ingore
d and branch coverage is 8/8.Also note that Throwable.addSuppressed(Throwable) cannot suppress itself, so the generated bytecode contains an additional guard (IF_ACMPEQ - reference equality) to prevent this). Luckily this branch is covered by the throw-on-write, throw-on-close and throw-on-write-and-close cases, as the bytecode variable slots are reused by the outer 2 of 3 exception handler regions.
This is not an issue with Jacoco - in fact the example code in the linked issue #82 is incorrect as there are no duplicated null checks and there is no nested catch block surrounding the close.
JUnit test demonstrating 8 of 8 branches covered
Caveat
Though not in OP's sample code, there is one case that can't be tested AFAIK.
If you pass the resource reference as an argument, then in Java 7/8 you must have a local variable to assign to:
In this case the generated code will still be guarding the resource reference. The syntatic sugar is updated in Java 9, where the local variable is no longer required:
try(arg){ /*...*/ }
Supplemental - Suggest use of library to avoid branches entirely
Admittedly some of these branches can be written off as unrealistic - i.e. where the try block uses the
AutoCloseable
without null checking or where the resource reference (with
) cannot be null.Often your application doesn't care where it failed - to open the file, write to it or close it - the granularity of failure is irrelevant (unless the app is specifically concerned with files, e.g. file-browser or word processor).
Furthermore, in the OP's code, to test the null closeable path - you'd have to refactor the try block into a protected method, subclass and provide a NOOP implementation - all this just get coverage on branches that will never be taken in the wild.
I wrote a tiny Java 8 library io.earcam.unexceptional (in Maven Central) that deals with most checked exception boilerplate.
Relevant to this question: it provides a bunch of zero-branch, one-liners for
AutoCloseable
s, converting checked exceptions to unchecked.Example: Free Port Finder
Jacoco has recently fixed this issue, Release 0.8.0 (2018/01/02)
"During creation of reports various compiler generated artifacts are filtered out, which otherwise require unnecessary and sometimes impossible tricks to not have partial or missed coverage:
http://www.jacoco.org/jacoco/trunk/doc/changes.html
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
And then disassemble, we get
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.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):
Java7 example (a try-with-resource approach):
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:
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.
i had a similar issue with something like this:
it complained that 2 of 8 branches weren't covered. ended up doing this:
no other changes and i now reached 100%....