Skip to content
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

Open
wants to merge 26 commits into
base: master
Choose a base branch
from

Conversation

Godin
Copy link
Member

@Godin Godin commented Apr 22, 2017

@Godin Godin added this to the 0.7.10 milestone Apr 22, 2017
@Godin Godin self-assigned this Apr 22, 2017
@Godin Godin added this to IN PROGRESS in Current work items Apr 22, 2017
@Godin Godin added this to TODO in Filtering Apr 22, 2017
@Godin Godin changed the title Merge duplicate finally blocks in a report During creation of report merge duplicate finally blocks that compiler generates Apr 23, 2017
@Godin Godin force-pushed the issue-521 branch 2 times, most recently from 7f2f506 to 5d9e92f Compare April 23, 2017 14:16
@Godin
Copy link
Member Author

Godin commented Apr 23, 2017

@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?

@Godin Godin requested a review from marchof April 23, 2017 19:28
@Godin Godin moved this from TODO to IN PROGRESS in Filtering Apr 23, 2017
*/
public int getCoveredBranches() {
public BitSet getCoveredBranches() {
Copy link
Member

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.

Copy link
Member Author

@Godin Godin Apr 24, 2017

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.

Copy link
Member

@marchof marchof Apr 24, 2017

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()).

Copy link
Member Author

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 the BitSet

I think that so far we don't have fields/methods that are "only for testing".

Copy link
Member

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.

Copy link
Member Author

@Godin Godin Apr 26, 2017

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Godin Thanks, LGTM now! 👍

@marchof
Copy link
Member

marchof commented Apr 24, 2017

@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,
Copy link
Member

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();
	}   

Copy link
Member Author

@Godin Godin Apr 27, 2017

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.

Copy link
Member

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()) {
Copy link
Member

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.

Copy link
Member Author

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) {
Copy link
Member

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?

Copy link
Member Author

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 {
Copy link
Member

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.

Copy link
Member Author

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 😉

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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) {
Copy link
Member

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)?

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

@Godin Godin Apr 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marchof done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Godin 👍 Thanks!

@marchof
Copy link
Member

marchof commented May 6, 2017

@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 AbstractMatcher) which by their API design cannot throw NPEs in the specific filter implementations.

@Godin Godin force-pushed the issue-521 branch 3 times, most recently from 70fdc19 to 19eb9cb Compare May 6, 2017 23:03
@marchof
Copy link
Member

marchof commented May 11, 2017

@Godin Another finding on the "big repo" by @p45q:

java.lang.IllegalStateException
at org.jacoco.core.internal.flow.Instruction.setPredecessor(Instruction.java:81)
at org.jacoco.core.internal.analysis.MethodAnalyzer.visitEnd(MethodAnalyzer.java:371)
at org.jacoco.core.internal.analysis.ClassAnalyzer$1.visitEnd(ClassAnalyzer.java:71)
at org.objectweb.asm.MethodVisitor.visitEnd(MethodVisitor.java:878)
at org.jacoco.core.internal.analysis.MethodAnalyzer.accept(MethodAnalyzer.java:142)
at org.jacoco.core.internal.flow.ClassProbesAdapter$2.visitEnd(ClassProbesAdapter.java:89)
at org.objectweb.asm.ClassReader.readMethod(ClassReader.java:1036)
at org.objectweb.asm.ClassReader.accept(ClassReader.java:708)
at org.objectweb.asm.ClassReader.accept(ClassReader.java:521)
at org.jacoco.core.analysis.Analyzer.analyzeClass(Analyzer.java:111)
at org.jacoco.core.analysis.Analyzer.analyzeClass(Analyzer.java:127)
... 5 more

Happens on a Eclipse EMF class of unknown version (packaged in some IBM product): ResourceImpl.zip

@Godin
Copy link
Member Author

Godin commented May 11, 2017

@marchof wow, thanks! However this one violates contract that was already here for method Instruction.setPredecessor, in this PR we just added check of this contract. Anyway I'll have a look.

@marchof
Copy link
Member

marchof commented May 11, 2017

@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.

@Godin
Copy link
Member Author

Godin commented May 11, 2017

@marchof here is what I've discovered for now:
This class also contains JSR and RET instructions, so that JSRInlinerAdapter kicks in before analysis. It adds labels, so that same instruction has multiple labels - comment about that was added in commit bead2e4 to MethodAnalyzer:

Due to ASM issue #315745 there can be more than one label per instruction

As a very simplified example of code after JSRInlinerAdapter for this class, which triggers IllegalStateException:

  ALOAD 1
  IFNE L1
  GOTO L2
 L1
 L2
  RETURN

And according to MethodAnalyzerTest in such case nextProbeId is 1, while expected 3 as in case with single label. So I have feeling that problem comes from LabelFlowAnalyzer that wasn't updated in commit bead2e4.

@Godin
Copy link
Member Author

Godin commented May 13, 2017

@marchof as was decided by phone: removed verification of contract of Instruction.setPredecessor and created separate ticket - #533

@Godin Godin force-pushed the issue-521 branch 2 times, most recently from 191fdf9 to 102707e Compare May 13, 2017 13:33
* 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.
Copy link
Member Author

@Godin Godin Aug 16, 2018

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
Copy link
Member Author

@Godin Godin Aug 16, 2018

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
Copy link
Member Author

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.

@Godin Godin moved this from Implementation to Review in Current work items Aug 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Filtering
  
In Progress
Development

Successfully merging this pull request may close these issues.

None yet

2 participants