尝试资源的 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
是 @Ingore
d,分支覆盖率是 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 中),它处理大多数检查异常样板。
与这个问题相关:它为AutoCloseable
s 提供了一堆零分支、单行代码,将已检查的异常转换为未检查的异常。
示例:免费端口查找器
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 覆盖吗?的主要内容,如果未能解决你的问题,请参考以下文章