尝试资源的 8 个分支 - 可以进行 jacoco 覆盖吗?

Posted

技术标签:

【中文标题】尝试资源的 8 个分支 - 可以进行 jacoco 覆盖吗?【英文标题】:8 branches for try with resources - jacoco coverage possible? 【发布时间】:2013-06-25 14:29:33 【问题描述】:

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

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

    不抛出任何异常, 从createStageList() 抛出异常 从processItem() 抛出异常 从close() 抛出异常 从processItem()close() 抛出异常

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

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

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

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

【问题讨论】:

你能发一下classfile吗? 不,我不能发布我客户的代码。 我用 Eclemma(Eclipse 中的 Emma)实现的最佳覆盖率是“错过了 8 个分支中的 3 个”,但 Jenkins 中的 Cobertura 仍然只显示 4/8。让我们希望这些覆盖工具很快就能正确处理 try-with-resources。 请注意,JaCoCo 无法完全涵盖的许多构造(例如这些构造)旨在帮助您减少代码中可能路径的数量(从而减少错误)。以 100% 的覆盖率为目标通常是不可能的,而且它也不会增加你的测试质量(但它确实需要付出很多努力)。 我的方法是简单地重写我的代码以不使用 try-with-resources 子句。考虑到它只是语法糖,并导致这种测试头痛,它并没有真正增加太多价值。 【参考方案1】:

好吧,我无法告诉您 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。我不得不使用 goto,因为字节码并不真正对应 Java 控制流。

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

【讨论】:

是的,我想知道是否某些生成的代码实际上无法访问,谢谢。如果 Oracle 能改进这一点当然会很好,或者覆盖工具会解决这个问题。 很好的解释,很有趣!现在我可以停止想我错过了什么。谢谢! 这里没有必要查看字节码(尽管这是一个有趣的练习)。 JLS 定义了 try-with-resources 等价于 Java 源代码:14.20.3.1. Basic try-with-resources,这样可以更容易地查看分支是什么。 @JoshuaTaylor JLS 仅定义语义等价。您仍然需要检查字节码以了解编译器是否按字面意思使用此策略。此外,您必须添加知识,现在(Java 7 强制),最终块被复制用于普通和异常情况,这使得在使用指定模式时测试变得多余。正如try with resources introduce unreachable bytecode 中所讨论的,这是一个javac 特定问题,例如Eclipse 的编译器不会产生无法访问的字节码。【参考方案2】:

我遇到了类似的问题:

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

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

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


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

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

【讨论】:

有趣,虽然已经很久了。事情可能已经改变,您使用什么工具和版本? 这不是问题中发布的 try-with-resources,而是包含条件的 try-finally。跨度> 【参考方案3】:

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

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

Old School 示例(最后尝试的方法):

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 个测试在 Old School 示例中获得 100% 的行、指令和分支覆盖率:

基本润滑脂路径(语句不为空,execute() 正常执行) execute() 抛出异常 foo() 抛出异常 AND 语句返回为 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% 的覆盖率IF(请参阅下面的 Matyas 回复)。但是,使用带有 Java8 JRE 的 Java7 编码风格,我相信您会遇到 6/8 分支。相同的代码,只是不同的 JRE。似乎在两个 JRE 之间创建字节码的方式不同,而 Java8 则创建了无法访问的路径。

【讨论】:

两个代码块产生的字节码完全不同 - try-with-resources 有 3 个异常处理区域,一个在 conn.createStatement() 之前开始,一个在主体周围,另一个在对 @987654325 的调用周围@。此外,还有对 Throwable.addSuppressed()if 的调用,以防止抑制相同的异常。【参考方案4】:

我可以覆盖所有 8 个分支,所以我的答案是肯定的。看看下面的代码,这只是一个快速的尝试,但它可以工作(或查看我的 github:https://github.com/bachoreczm/basicjava 和 'trywithresources' 包,在那里你可以找到 try-with-resources 的工作原理,请参阅 '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;
  

【讨论】:

您的 autoClose 方法没有 catch 块。这不是同一种情况(通常一个人不测量测试类本身的覆盖率?)此外,如果您想声称成功,显示它被覆盖的 jacoco 输出的屏幕截图会很好。 我附上了截图,是的,看测试类的覆盖率(在 try-with-resources-end 行,你会看到 8/8)。 我还附上了一个链接,您可以在其中找到确切的描述,以及 try-with-resources 的工作原理。 我认为 catch 块与覆盖问题无关。 那么为什么不添加一个异常捕获并消除所有疑问呢?【参考方案5】:

四岁了,但还是……

    具有非空AutoCloseable 的快乐路径 带有 null AutoCloseable 的快乐路径 写入时抛出 关闭时抛出 写入和关闭时抛出 抛出资源规范(with 部分,例如构造函数调用) 抛出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 的问题 - 事实上,链接issue #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) 
            //...
        
    

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

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

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

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

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

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

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

示例:免费端口查找器

int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort);

【讨论】:

问题是您正在查看 Eclipse 生成的代码以消除 javac 生成的代码引发的问题。说“如果无法访问它们,那么根据定义,它就是编译器错误”有点苛刻,因为规范并没有保证字节码没有无法访问的代码。在正常情况下,您根本不会注意到。这不是 javac 生成无法访问代码的唯一地方,例如我在野外见过过时的access$… 方法。幸运的是,JDK 11 解决了这两个问题。另请参阅 JDK-8194978。【参考方案6】:

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

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

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

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

【讨论】:

以上是关于尝试资源的 8 个分支 - 可以进行 jacoco 覆盖吗?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin协程的Jacoco代码覆盖率不正确

代码覆盖率-JaCoCo

jacoco增量代码覆盖率

Android Studio (3.2.1) Instant Run 不适用于 Jacoco 0.8.2?

如何验证某些排除类和jacoco插件的最小覆盖率?

执行 JaCoCo 时出现“由于缺少执行数据文件而跳过 JaCoCo 执行”