-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
During creation of report merge duplicate finally blocks that compiler generates #521
base: master
Are you sure you want to change the base?
Conversation
7f2f506
to
5d9e92f
Compare
@marchof To merge conditional jumps where different branches are covered in different instructions we need to determine which branch covered - this is what first change does. Second introduces actual merging. While there is some room for improvements, changes are pretty straightforward, and so main care should be paid for third change that introduces filter for finally blocks. Thus wondering what you think about it? |
*/ | ||
public int getCoveredBranches() { | ||
public BitSet getCoveredBranches() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin I would prefer to keep the BitSet internally. So
- Keep
int getCoveredBranches()
- Add
void merge(Instruction)
This encapsulation looks cleaner to me allows internal optimizations later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
allows internal optimizations later
I suppose internal in respect to a class and not project since class is already in internal package 😉
Jokes aside - while indeed such encapsulation looks cleaner, this will complicate a test which currently able to directly assert on a content of this BitSet
, but ok can do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course internal to the internal class 😉 . For whitebox testing of Instruction
we can still expose the BitSet
but for interaction with other classes I would prefer a more semantic interface (like merge()
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For whitebox testing of
Instruction
we can still expose theBitSet
I think that so far we don't have fields/methods that are "only for testing".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the only ones are in LabelFlowAnalyzer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof encapsulated, added toString
instead of package-local field to avoid its accidental usage in other classes
✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin Thanks, LGTM now! 👍
@Godin I definitely like the straightforward approach! I will go though the implementation and add comments if I have specific questions or concerns. |
*/ | ||
public void setPredecessor(final Instruction predecessor) { | ||
public void setPredecessor(final Instruction predecessor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to this change but while trying to understand my own code after some time I wondered, why a instruction can't have multiple predecessors: While this is of course possible in byte code JaCoCo adds probes in a way that each instruction has at most one predecessor. Otherwise probes are inserted. Maybe we can improve documentation here:
Sets the given instruction as a direct predecessor to this instruction. Every instruction can have at most one direct predecessor otherwise probes are inserted in-between. This will add an branch to the predecessor.
What about adding a assertion to enforce this invariant?
if (this.predecessor != null) {
throw new AssertionError();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof I also was puzzled by this multiple times.
Maybe terms "direct predecessor" and "in-between" are a bit vague: in case of
L0: IFEQ L2
L1: NOP
L2: NOP
L1
will not be marked as predecessor of L2
and there is probe "in-between" them
L2
will be marked as predecessor of L0
, however not so "direct" (i.e. not a previous instruction) - via jump and there is an "in-between" probe that is executed on this jump from L0
to L2
😉
But your proposal anyway better than nothing.
I updated javadoc and added check of invariant using a slightly modified version of your proposal.
✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin Thanks, hopefully helps us and others with this tricky piece of code in future.
private static boolean isSame(final int size, AbstractInsnNode e, | ||
AbstractInsnNode n) { | ||
for (int i = 0; i < size; i++) { | ||
if (e.getOpcode() != n.getOpcode()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about also checking operands here? Should be the same except for labels.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof turns out that not only for labels: for example for
class Fun {
static void nop(String s) {
}
void fun() {
try {
nop("1");
} finally {
try {
nop("2");
} catch (Exception e) {
nop("catch " + e);
} finally {
nop("finally");
}
}
}
}
javac will generate code that uses astore 1
and aload 1
on non exceptional path, while astore 4
and aload 4
on exceptional path. This can be easily seen in LocalVariableTable
, which we can't use during comparison:
LocalVariableTable:
Start Length Slot Name Signature
19 22 1 e Ljava/lang/Exception;
76 23 4 e Ljava/lang/Exception;
and now I recall that I already saw this during implementation of filter for try-with-resources
. So let's have a talk about this tomorrow at GenevaJUG.
*/ | ||
public class Finally { | ||
|
||
private static void ex(boolean t) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this to org.jacoco.core.test.validation.targets.Stubs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof done
/** | ||
* This test target is a finally block. | ||
*/ | ||
public class Finally { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder whether filter works for nested finally blocks too. These create really ugly flows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof Do you think about some particular example of an "ugly flow"? or just nested? in any case we can talk/test tomorrow at GenevaJUG 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just nested: A try/finally block within a finally block. This is probably too much 😉 .
Also a test for finally blocks with some control structures (e.g. if
) would be a nice test for the isSame()
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact we already have finally of try-with-resources nested into finally of another try-with-resources and this new filter changed assertion there 😉 but indeed - can add explicit one here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof About control structures such as if
: there is already test of conditional jump that is generated for nop(i == 0)
, so I'm hesitating with addition of test for if
statement - in this case why not add tests for other statements? and what about their relative order and so on? i.e. how to not go mad trying to generate all possible variants that are infinite in general.
Nested finally
in his turn shows difference of slots for different duplicates of same finally
block, which is not shown in existing tests here, so added it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin Ok, this is enough of control structures for now.
try { | ||
instruction.setPredecessor(predecessor, 0); | ||
fail("exception expected"); | ||
} catch (IllegalStateException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin Why not @Test(expected=IllegalStateException.class)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof to isolate operation that throws since there was already code in this test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin Ah I see. But in this case I would prefer to have a separate test case specifically for this scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof done
✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin 👍 Thanks!
@Godin I wouldn't consider or test functional correctness for compiler versions < 1.5. But our code should not throw exceptions on any valid class file supplied. As a side note I think all matching should happen with well-unit-tested matching primitives (like provided by |
70fdc19
to
19eb9cb
Compare
@Godin Another finding on the "big repo" by @p45q:
Happens on a Eclipse EMF class of unknown version (packaged in some IBM product): ResourceImpl.zip |
@marchof wow, thanks! However this one violates contract that was already here for method |
@Godin That's why I was also considering opening a separate issue. If this is a bigger think we should move the assertion and fix into a separate PR. |
@marchof here is what I've discovered for now:
As a very simplified example of code after
And according to |
191fdf9
to
102707e
Compare
* Note that difference of this case from {@link #breakStatement()} is that | ||
* there is nothing between <code>break</code> statement and end of | ||
* <code>try</code> block. And current implementation of filter does not | ||
* handle such case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems that filter that was implemented in #604 handles this case 😋
/** | ||
* Note that javac generates <code>goto</code> instruction at the end of | ||
* <code>while</code> statement that refers to a line of previous | ||
* instruction. And so causes partial coverage of last line of finally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems that we forgot to preserve this case in #604.
|
||
/** | ||
* Note that in this case javac generates bytecode that uses different slots | ||
* in different duplicates of same finally block for variable that stores |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems that we forgot to preserve this or at least similar comment in #604.
https://github.com/jacoco/jacoco/wiki/filtering-JAVAC.FINALLY