有资源尝试的8个分支机构 - 可能覆盖jacoco吗?

Gus*_*Gus 60 java code-coverage bytecode try-with-resources jacoco

我有一些代码使用try资源和jacoco它只有一半覆盖.所有的源代码行都是绿色的,但我得到一个黄色的小符号告诉我8个分支中只有4个被覆盖.

在此输入图像描述

我无法弄清楚所有分支是什么,以及如何编写涵盖它们的代码.扔三个可能的地方PipelineException.这些createStageList(),processItem()以及隐含的close()

  1. 没有抛出任何例外,
  2. 抛出异常 createStageList()
  3. 抛出异常 processItem()
  4. 抛出异常 close()
  5. processItem()和抛出异常close()

我想不出任何其他情况,但我仍然只有8个中有4个被覆盖.

有人可以向我解释为什么它是8中的4,并且无论如何都可以击中所有8个分支?我不熟悉decyrpting /阅读/解释字节码,但也许你是...... :)我已经看过https://github.com/jacoco/jacoco/issues/82,但它既不是也不是问题它非常引用帮助(除了注意这是由于编译器生成的块)

嗯,就在我写完这篇文章的时候,我想到了上面提到的那些案例可能没有被测试过......如果我做对了,我会发一个答案.我确信这个问题和答案在任何情况下都会对某人有所帮助.

编辑:没有,我没有找到它.抛出RuntimeExceptions(不由catch块处理)不包括任何更多分支

Ant*_*ony 56

好吧,我不能告诉你Jacoco的确切问题是什么,但我可以告诉你如何编译Try With Resources.基本上,有很多编译器生成的开关来处理在各个点抛出的异常.

如果我们采用以下代码并编译它

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";
}
Run Code Online (Sandbox Code Playgroud)

然后拆卸,我们得到

.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_1
L3:
    new java/io/CharArrayWriter
    dup
    invokespecial java/io/CharArrayWriter <init> ()V
    astore_2
    aconst_null
    astore_3
L13:
    aload_2
    aconst_null
    invokevirtual java/io/CharArrayWriter writeTo (Ljava/io/Writer;)V
L18:
    aload_2
    ifnull L94
    aload_3
    ifnull L44
L26:
    aload_2
    invokevirtual java/io/CharArrayWriter close ()V
L30:
    goto L94
L33:
.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 L94
L44:
.stack same
    aload_2
    invokevirtual java/io/CharArrayWriter close ()V
    goto L94
L51:
.stack same_locals_1_stack_item
    stack Object java/lang/Throwable
.end stack
    astore 4
    aload 4
    astore_3
    aload 4
    athrow
L59:
.stack same_locals_1_stack_item
    stack Object java/lang/Throwable
.end stack
    astore 5
L61:
    aload_2
    ifnull L91
    aload_3
    ifnull L87
L69:
    aload_2
    invokevirtual java/io/CharArrayWriter close ()V
L73:
    goto L91
L76:
.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 L91
L87:
.stack same
    aload_2
    invokevirtual java/io/CharArrayWriter close ()V
L91:
.stack same
    aload 5
    athrow
L94:
.stack full
    locals Object [Ljava/lang/String; Object java/lang/String
    stack 
.end stack
    goto L108
L97:
.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;)V
L108:
.stack same
    ldc 'after'
    astore_2
    return
.end method
Run Code Online (Sandbox Code Playgroud)

对于那些不会说字节码的人来说,这大致相当于下面的伪Java.我不得不使用gotos,因为字节码并不真正对应Java控制流.

如您所见,有很多情况可以处理抑制异常的各种可能性.能够涵盖所有这些案件是不合理的.实际上,goto L59第一个try块上的分支是不可能达到的,因为第一个捕获Throwable将捕获所有异常.

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)
}
Run Code Online (Sandbox Code Playgroud)

  • 这里没有必要查看字节码(虽然这是一个有趣的练习).根据Java源代码,JLS定义了try-with-resources相当于什么:[14.20.3.1.基本尝试资源](https://docs.oracle.com/javase/specs/jls/se7/html/jls-14.html#jls-14.20.3.1),这样可以更容易地看到分支是. (6认同)
  • @JoshuaTaylor JLS 仅定义语义等价。您仍然需要检查字节码以了解编译器是否按字面意思使用此策略。此外,您必须添加知识,现在(Java 7 强制要求),finally 块被复制用于普通和异常情况,这使得在使用指定模式时测试变得多余。正如[尝试使用资源引入无法访问的字节码](/sf/ask/1793079221/) 中所述,这是一个`javac` 特定问题,例如Eclipse 的编译器不会产生无法访问的字节码。 (4认同)

小智 8

在此输入图像描述

我可以覆盖所有8个分支,所以我的答案是肯定的.看看下面的代码,这只是一个快速尝试,但它的工作原理(或者看看我的github:https://github.com/bachoreczm/basicjava和'trywithresources'包,你可以找到,如何尝试 -资源有效,请参阅'explainOfTryWithResources'类):

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;
  }
}
Run Code Online (Sandbox Code Playgroud)


Jef*_*ett 6

没有真正的问题,但想要在那里投入更多的研究.tl; dr =看起来你可以为try-finally实现100%的覆盖率,但不能用于try-with-resource.

可以理解的是,老派的try-finally和Java7 try-with-resources之间存在差异.这是两个使用替代方法显示相同内容的等效示例.

老派的例子(尝试终极方法):

final Statement stmt = conn.createStatement();
try {
    foo();
    if (stmt != null) {
        stmt.execute("SELECT 1");
    }
} finally {
    if (stmt != null)
        stmt.close();
}
Run Code Online (Sandbox Code Playgroud)

Java7示例(尝试使用资源的方法):

try (final Statement stmt = conn.createStatement()) {
    foo();
    if (stmt != null) {
        stmt.execute("SELECT 1");
    }
}
Run Code Online (Sandbox Code Playgroud)

分析:老派示例:
使用Jacoco 0.7.4.201502262128和JDK 1.8.0_45,我可以使用以下4个测试获得Old School示例的100%行,指令和分支覆盖:

  • 基本润滑脂路径(语句不为null,执行()正常运行)
  • execute()抛出异常
  • foo()抛出异常AND语句返回为null
  • 语句返回null
Jacoco在'try'(在空检查中)内部指示2个分支,在finally内部指示4个分支(在空检查上).所有都完全覆盖.

分析:java-7示例:
如果针对Java7样式示例运行相同的4个测试,则jacoco表示覆盖了6/8个分支(在try本身上)和在try中的null-check上的2/2.我尝试了一些额外的测试以增加覆盖率,但我发现没有办法比6/8更好.正如其他人所指出的那样,java-7示例的反编译代码(我也看到了)表明java编译器正在为try-with-resource生成无法访问的段.Jacoco正在(准确地)报告存在此类细分市场.

更新:使用Java7的编码风格,你也许可以得到100%的覆盖率IF使用Java7 JRE(见下文马加什响应).但是,使用带有Java8 JRE的Java7编码风格,我相信你会覆盖6/8分支机构.相同的代码,只是不同的JRE.看起来两个JRE之间的字节代码创建方式不同,Java8创建了无法访问的路径.


ear*_*cam 5

四岁了,但还是......

  1. 非空的快乐路径 AutoCloseable
  2. 与null的愉快的道路 AutoCloseable
  3. 扔在写
  4. 投掷结束
  5. 投掷并关闭
  6. 引发资源规范(包含部分,例如构造函数调用)
  7. try块中抛出但是AutoCloseable为空

以上列出了所有7个条件 - 8个分支的原因是由于重复条件.

可以到达所有分支,try-with-resources相当简单的编译器糖(至少比较switch-on-string) - 如果它们无法到达,那么它根据定义是一个编译器bug.

只有6单元测试实际需要(在下面的示例代码,throwsOnClose@Ingored和分支覆盖是8/8.

另请注意,Throwable.addSuppressed(Throwable)无法抑制自身,因此生成的字节码包含一个额外的保护(IF_ACMPEQ - 引用相等)来防止这种情况.幸运的是,这个分支由throw-on-write,throw-on-close和throw-on-write-and-close情况所覆盖,因为字节码变量槽由3个异常处理程序区域中的外部2个重用.

不是 Jacoco的问题 - 实际上链接问题#82中的示例代码是不正确的,因为没有重复的空检查,并且关闭周围没有嵌套的catch块.

JUnit测试展示了8个分支中的8个

import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;

import org.junit.Ignore;
import org.junit.Test;

public class FullBranchCoverageOnTryWithResourcesTest {

    private static class DummyOutputStream extends OutputStream {

        private final IOException thrownOnWrite;
        private final IOException thrownOnClose;


        public DummyOutputStream(IOException thrownOnWrite, IOException thrownOnClose)
        {
            this.thrownOnWrite = thrownOnWrite;
            this.thrownOnClose = thrownOnClose;
        }


        @Override
        public void write(int b) throws IOException
        {
            if(thrownOnWrite != null) {
                throw thrownOnWrite;
            }
        }


        @Override
        public void close() throws IOException
        {
            if(thrownOnClose != null) {
                throw thrownOnClose;
            }
        }
    }

    private static class Subject {

        private OutputStream closeable;
        private IOException exception;


        public Subject(OutputStream closeable)
        {
            this.closeable = closeable;
        }


        public Subject(IOException exception)
        {
            this.exception = exception;
        }


        public void scrutinize(String text)
        {
            try(OutputStream closeable = create()) {
                process(closeable);
            } catch(IOException e) {
                throw new UncheckedIOException(e);
            }
        }


        protected void process(OutputStream closeable) throws IOException
        {
            if(closeable != null) {
                closeable.write(1);
            }
        }


        protected OutputStream create() throws IOException
        {
            if(exception != null) {
                throw exception;
            }
            return closeable;
        }
    }

    private final IOException onWrite = new IOException("Two writes don't make a left");
    private final IOException onClose = new IOException("Sorry Dave, we're open 24/7");


    /**
     * Covers one branch
     */
    @Test
    public void happyPath()
    {
        Subject subject = new Subject(new DummyOutputStream(null, null));

        subject.scrutinize("text");
    }


    /**
     * Covers one branch
     */
    @Test
    public void happyPathWithNullCloseable()
    {
        Subject subject = new Subject((OutputStream) null);

        subject.scrutinize("text");
    }


    /**
     * Covers one branch
     */
    @Test
    public void throwsOnCreateResource()
    {
        IOException chuck = new IOException("oom?");
        Subject subject = new Subject(chuck);
        try {
            subject.scrutinize("text");
            fail();
        } catch(UncheckedIOException e) {
            assertThat(e.getCause(), is(sameInstance(chuck)));
        }
    }


    /**
     * Covers three branches
     */
    @Test
    public void throwsOnWrite()
    {
        Subject subject = new Subject(new DummyOutputStream(onWrite, null));
        try {
            subject.scrutinize("text");
            fail();
        } catch(UncheckedIOException e) {
            assertThat(e.getCause(), is(sameInstance(onWrite)));
        }
    }


    /**
     * Covers one branch - Not needed for coverage if you have the other tests
     */
    @Ignore
    @Test
    public void throwsOnClose()
    {
        Subject subject = new Subject(new DummyOutputStream(null, onClose));
        try {
            subject.scrutinize("text");
            fail();
        } catch(UncheckedIOException e) {
            assertThat(e.getCause(), is(sameInstance(onClose)));
        }
    }


    /**
     * Covers two branches
     */
    @SuppressWarnings("unchecked")
    @Test
    public void throwsOnWriteAndClose()
    {
        Subject subject = new Subject(new DummyOutputStream(onWrite, onClose));
        try {
            subject.scrutinize("text");
            fail();
        } catch(UncheckedIOException e) {
            assertThat(e.getCause(), is(sameInstance(onWrite)));
            assertThat(e.getCause().getSuppressed(), is(arrayContaining(sameInstance(onClose))));
        }
    }


    /**
     * Covers three branches
     */
    @Test
    public void throwsInTryBlockButCloseableIsNull() throws Exception
    {
        IOException chucked = new IOException("ta-da");
        Subject subject = new Subject((OutputStream) null) {
            @Override
            protected void process(OutputStream closeable) throws IOException
            {
                throw chucked;
            }
        };

        try {
            subject.scrutinize("text");
            fail();
        } catch(UncheckedIOException e) {
            assertThat(e.getCause(), is(sameInstance(chucked)));
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

Eclipse Coverage

警告

虽然不在OP的示例代码中,但有一个案例无法通过AFAIK进行测试.

如果将资源引用作为参数传递,那么在Java 7/8中,您必须具有要分配的局部变量:

    void someMethod(AutoCloseable arg)
    {
        try(AutoCloseable pfft = arg) {
            //...
        }
    }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,生成的代码仍将保护资源引用.合成糖在Java 9中更新,其中不再需要局部变量:try(arg){ /*...*/ }

补充 - 建议使用库来完全避免分支

不可否认,这些分支中的一些可以被写为不切实际 - 即try块使用AutoCloseable无空值检查或资源引用(with)不能为空的位置.

通常,您的应用程序并不关心它失败的地方 - 打开文件,写入文件或关闭它 - 失败的粒度是无关紧要的(除非应用程序特别关注文件,例如文件浏览器或文字处理器).

此外,在OP的代码中,要测试null可关闭路径 - 您必须将try块重构为受保护的方法,子类并提供NOOP实现 - 所有这些只是覆盖了永远不会在野外采用的分支.

我写了一个小的Java 8库io.earcam.unexceptional(在Maven Central中),它处理大多数已检查的异常样板.

与此问题相关:它为AutoCloseables 提供了一堆零分支单行,将已检查的异常转换为未选中.

示例:免费端口查找器

int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort);
Run Code Online (Sandbox Code Playgroud)

  • 问题是您正在查看 Eclipse 生成的代码以消除 javac 生成的代码引发的问题。说“*如果无法访问它们,那么根据定义它就是编译器错误*”有点苛刻,因为规范没有保证字节码不存在无法访问的代码。在正常情况下,你根本不会注意到。这并不是 javac 生成无法访问的代码的唯一地方,例如,我在野外见过过时的“access$...”方法。值得庆幸的是,这两个问题在 JDK 11 中都消失了。另请参阅 [JDK-8194978](https://bugs.openjdk.java.net/browse/JDK-8194978)。 (2认同)