IT TIP

리소스로 시도하기위한 8 가지 지점-Jacoco 커버리지 가능?

itqueen 2020. 12. 1. 20:19
반응형

리소스로 시도하기위한 8 가지 지점-Jacoco 커버리지 가능?


try with resources를 사용하는 코드가 있으며 jacoco에서는 절반으로 만 표시됩니다. 모든 소스 코드 줄이 녹색이지만 8 개 분기 중 4 개만 포함되어 있음을 알려주는 노란색 기호가 표시됩니다.

여기에 이미지 설명 입력

나는 모든 분기가 무엇인지, 그리고 그것들을 다루는 코드를 작성하는 방법을 파악하는 데 어려움을 겪고 있습니다. 세 가지 가능한 장소가 던져 PipelineException집니다. 이들은 createStageList(), processItem()그리고는 암시close()

  1. 예외를 던지지 않고
  2. 예외 던지기 createStageList()
  3. 예외 던지기 processItem()
  4. 예외 던지기 close()
  5. 에서 예외를 throw processItem()close()

다른 케이스는 생각할 수 없지만 여전히 8 개 중 4 개만 다룹니다.

누군가가 왜 그것이 4/8이고 어쨌든 8 브랜치를 모두 칠 수 있는지 설명해 줄 수 있습니까? 나는 바이트 코드 해독 / 읽기 / 해석에 능숙하지 않지만 아마도 당신은 ... :) 나는 이미 https://github.com/jacoco/jacoco/issues/82 봤지만 그것도 문제도 아닙니다 (컴파일러가 생성 한 블록 때문이라는 점을 제외하고는) 도움말을 매우 많이 참조합니다.

흠,이 글을 다 쓰는 순간 위에서 언급 한 내용으로 테스트되지 않은 케이스가 무엇인지 생각해 보았습니다. 정답을 맞히면 답변을 게시하겠습니다. 나는이 질문과 대답이 어떤 경우에도 도움이 될 것이라고 확신합니다.

편집 : 아니요, 찾지 못했습니다. 발생하는 RuntimeExceptions (catch 블록에 의해 처리되지 않음)가 더 이상 분기를 다루지 않았습니다.


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와 동일합니다. 바이트 코드가 실제로 Java 제어 흐름에 해당하지 않기 때문에 gotos를 사용해야했습니다.

보시다시피, 억제 된 예외의 다양한 가능성을 처리하는 많은 경우가 있습니다. 이러한 모든 경우를 다룰 수있는 것은 합리적이지 않습니다. 사실, goto L59첫 번째 시도 블록 분기는 도달 할 수 없습니다. 첫 번째 Throwable catch는 모든 예외를 포착하기 때문입니다.

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

여기에 이미지 설명 입력

8 개 지점을 모두 커버 할 수 있으므로 내 대답은 YES입니다. 다음 코드를보세요. 이것은 빠른 시도 일 뿐이지 만 작동합니다 (또는 내 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;
  }
}

진짜 질문은 아니지만 더 많은 연구를하고 싶었습니다. 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();
}

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 % 라인, 교육 및 분기 범위를 얻을 수있었습니다.

  • 기본 그리스 경로 (null이 아니고 execute ()가 정상적으로 실행 됨)
  • execute ()에서 예외 발생
  • foo ()는 예외 AND 문을 null로 반환합니다.
  • null로 반환 된 문
Jacoco는 'try'(null 검사에서) 내부에 2 개의 분기를 나타내고, finally (null 검사에서) 내부에 4 개를 나타냅니다. 모두 완전히 커버됩니다.

Analysis: java-7 example:
If the same 4 tests run against the Java7 style example, jacoco indicates 6/8 branches are covered (on the try itself) and 2/2 on the null-check within the try. I tried a number of additional tests to increase coverage, but I can find no way to get better than 6/8. As others have indicated, the decompiled code (which I did also look at) for the java-7 example suggests that the java compiler is generating unreachable segments for try-with-resource. Jacoco is reporting (accurately) that such segments exist.

업데이트 : Java7 코딩 스타일을 사용하여, 당신은 100 % 혜택을받을 수있을 경우 Java7 JRE (아래 마 탸스 응답 참조)을 사용. 그러나 Java8 JRE와 함께 Java7 코딩 스타일을 사용하면 6/8 브랜치를 다룰 것이라고 믿습니다. 동일한 코드, 다른 JRE. Java8이 도달 할 수없는 경로를 생성하는 두 JRE간에 바이트 코드가 다르게 생성되는 것 같습니다.


네 살이지만 여전히 ...

  1. null이 아닌 행복한 경로 AutoCloseable
  2. null로 행복한 길 AutoCloseable
  3. 쓰기시 발생
  4. 닫기에 던짐
  5. 쓰기 및 닫기시 발생
  6. 리소스 사양에서 발생 ( with 부분, 예 : 생성자 호출)
  7. try블록 에서 던지지 만 AutoCloseablenull입니다.

위의 7 가지 조건이 모두 나열되어 있습니다. 8 개 분기의 이유는 반복 된 조건 때문입니다.

모든 브랜치에 도달 할 수 있습니다. 이는 try-with-resources상당히 단순한 컴파일러 설탕입니다 (적어도에 비해 switch-on-string). 도달 할 수없는 경우 정의상 컴파일러 버그입니다.

단지 6 단위 테스트 실제로 아래 예 코드 (필요한, throwsOnClose@IngoreD 지점에 따르면이 8/8이다.

또한 Throwable.addSuppressed (Throwable) 는 자신을 억제 할 수 없으므로 생성 된 바이트 코드에는이를 방지하기위한 추가 가드 (IF_ACMPEQ-참조 동등성)가 포함되어 있습니다. 다행히도이 분기는 쓰기시 발생, 닫기시 발생 및 쓰기시 발생 및 닫기 사례로 처리됩니다. 바이트 코드 변수 슬롯은 3 개의 예외 처리기 영역 중 외부 2 개에서 재사용되기 때문입니다.

이것은 Jacoco의 문제 아닙니다 . 실제로 연결된 문제 # 82 의 예제 코드는 중복 된 null 검사가없고 닫기를 둘러싼 중첩 된 catch 블록이 없기 때문에 올바르지 않습니다.

8 개 분기 중 8 개가 포함 된 JUnit 테스트

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

    }
}

Eclipse 적용 범위

경고

OP의 샘플 코드는 아니지만 AFAIK를 테스트 할 수없는 경우가 하나 있습니다.

리소스 참조를 인수로 전달하면 Java 7/8에서 할당 할 로컬 변수가 있어야합니다.

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

이 경우 생성 된 코드는 여전히 리소스 참조를 보호합니다. Syntatic sugar는 Java 9에서 업데이트되어 로컬 변수가 더 이상 필요하지 않습니다.try(arg){ /*...*/ }

보충-분기를 완전히 피하기 위해 라이브러리 사용 제안

Admittedly some of these branches can be written off as unrealistic - i.e. where the try block uses the AutoCloseable without null checking or where the resource reference (with) cannot be null.

Often your application doesn't care where it failed - to open the file, write to it or close it - the granularity of failure is irrelevant (unless the app is specifically concerned with files, e.g. file-browser or word processor).

Furthermore, in the OP's code, to test the null closeable path - you'd have to refactor the try block into a protected method, subclass and provide a NOOP implementation - all this just get coverage on branches that will never be taken in the wild.

I wrote a tiny Java 8 library io.earcam.unexceptional (in Maven Central) that deals with most checked exception boilerplate.

Relevant to this question: it provides a bunch of zero-branch, one-liners for AutoCloseables, converting checked exceptions to unchecked.

Example: Free Port Finder

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

Jacoco has recently fixed this issue, Release 0.8.0 (2018/01/02)

"During creation of reports various compiler generated artifacts are filtered out, which otherwise require unnecessary and sometimes impossible tricks to not have partial or missed coverage:

  • Part of bytecode for try-with-resources statements (GitHub #500)."

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


i had a similar issue with something like this:

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

it complained that 2 of 8 branches weren't covered. ended up doing this:

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

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

다른 변화가 없었고 지금은 100 %에 도달했습니다 ....

참고 URL : https://stackoverflow.com/questions/17354150/8-branches-for-try-with-resources-jacoco-coverage-possible

반응형