62

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

在此处输入图像描述

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

  1. 不抛出任何异常,
  2. 抛出异常createStageList()
  3. 抛出异常processItem()
  4. 抛出异常close()
  5. processItem()从和抛出异常close()

我想不出任何其他案例,但我仍然只覆盖了 8 个案例中的 4 个。

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

嗯,就在我写完这篇文章时,我想到了我上面提到的可能无法测试哪些情况......如果我做对了,我会发布一个答案。我相信这个问题及其答案无论如何都会对某人有所帮助。

编辑:不,我没有找到它。抛出 RuntimeExceptions(不由 catch 块处理)不再覆盖任何分支

4

6 回答 6

57

好吧,我无法告诉您 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";
}

然后拆解,我们得到

.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

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

如您所见,有很多情况可以处理被抑制异常的各种可能性。能够涵盖所有这些情况是不合理的。事实上,goto L59第一个 try 块上的分支是不可能到达的,因为第一个 catch 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)
}
于 2013-06-28T03:23:15.433 回答
9

在此处输入图像描述

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

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;
  }
}
于 2016-03-01T14:09:45.013 回答
6

没有真正的问题,但想在那里进行更多研究。tl;dr = 看起来你可以为 try-finally 实现 100% 的覆盖率,但对于 try-with-resource 则不行。

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

老派的例子(一个try-finally方法):

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

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

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

分析:老派示例:
使用 Jacoco 0.7.4.201502262128 和 JDK 1.8.0_45,我能够使用以下 4 个测试在老派示例中获得 100% 的行、指令和分支覆盖率:

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

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

更新:使用 Java7 编码风格,如果使用 Java7 JRE ,您可能能够获得 100% 的覆盖率(请参阅下面的 Matyas 响应)。但是,使用带有 Java8 JRE 的 Java7 编码风格,我相信您会遇到 6/8 分支。相同的代码,只是不同的 JRE。似乎在两个 JRE 之间创建的字节码不同,而 Java8 则创建了无法访问的路径。

于 2015-08-31T17:34:10.970 回答
6

四岁了,还是...

  1. 非空的快乐路径AutoCloseable
  2. null 的快乐路径AutoCloseable
  3. 写入时抛出
  4. 近距离投掷
  5. 写入并关闭
  6. 抛出资源规范(with部分,例如构造函数调用)
  7. 抛出try块但AutoCloseable为空

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

可以到达所有分支,这try-with-resources是相当简单的编译器糖(至少与 相比switch-on-string)-如果无法到达,则根据定义它是编译器错误。

实际上只需要 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)));
        }

    }
}

日食覆盖率

警告

虽然不在 OP 的示例代码中,但有一种情况无法测试 AFAIK。

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

    void someMethod(AutoCloseable arg)
    {
        try(AutoCloseable pfft = arg) {
            //...
        }
    }

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

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

诚然,这些分支中的一些可以被认为是不切实际的 - 即 try 块使用AutoCloseable不带空检查的地方或资源引用 ( with) 不能为空的地方。

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

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

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

与这个问题相关:它为 s 提供了一堆零分支、单行代码AutoCloseable,将已检查的异常转换为未检查的异常。

示例:自由端口查找器

int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort);
于 2017-10-22T19:54:12.527 回答
2

Jacoco 最近修复了这个问题,版本 0.8.0 (2018/01/02)

“在创建报告期间,各种编译器生成的工件被过滤掉,否则需要不必要的,有时甚至是不可能的技巧来避免部分或遗漏的覆盖:

  • try-with-resources 语句的部分字节码 (GitHub #500)。”

http://www.jacoco.org/jacoco/trunk/doc/changes.html

于 2018-04-19T19:36:15.137 回答
1

我遇到了类似的问题:

try {
...
} finally {
 if (a && b) {
  ...
 }
}

它抱怨没有覆盖8个分支机构中的2个。最终这样做了:

try {
...
} finally {
 ab(a,b);
}

void ab(a, b) {
 if (a && b) {
...
 }
}

没有其他变化,我现在达到了 100%....

于 2014-08-29T18:31:44.880 回答