Подтвердить что ты не робот

8 ветвей для попыток с ресурсами - возможно ли использование jacoco?

У меня есть код, который использует try с ресурсами, а в jacoco он появляется только наполовину. Все строки исходного кода зеленые, но я получаю маленький желтый символ, говорящий мне, что покрыто только 4 из 8 ветвей.

enter image description here

Мне трудно понять, что такое все ветки, и как писать код, который их охватывает. Три возможных места бросают PipelineException. Это createStageList(), processItem() и подразумеваемый close()

  • Не исключение исключений,
  • исключение из createStageList()
  • исключение из processItem()
  • сброс исключения из close()
  • выброс исключения из processItem() и close()

Я не могу думать о каких-либо других случаях, но у меня все еще есть только 4 из 8.

Может кто-нибудь объяснить мне, почему это 4 из 8, и все равно ударить все 8 ветвей? Я не умею дешифровать/читать/интерпретировать байтовый код, но, может быть, вы...:) Я уже видел https://github.com/jacoco/jacoco/issues/82, но ни он, ни проблема, с которой он ссылается, очень помогает (кроме того, что это связано с блоками, сгенерированными компилятором)

Хм, точно так же, как я закончил писать, у меня возникла мысль о том, какие случаи могут быть не проверены по тому, о чем я упоминал выше... Я отправлю ответ, если я это исправлю. Я уверен, что этот вопрос и ответ ответят кому-то в любом случае.

EDIT: Нет, я не нашел его. Бросок RuntimeExceptions (не обрабатывается блоком catch) больше не распространяется на ветки

4b9b3361

Ответ 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. Мне пришлось использовать 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)
}

Ответ 2

enter image description here

Я могу покрыть все 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;
  }
}

Ответ 3

Нет реального вопроса, но он хотел бросить больше исследований. tl; dr = Похоже, вы можете достичь 100% -ного охвата для try-finally, но не для 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-with-resource):

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

Анализ: пример старой школы:
Используя Jacoco 0.7.4.201502262128 и JDK 1.8.0_45, я смог получить 100% -ную линейную, командную и отраслевую поддержку на примере старой школы, используя следующие 4 теста:

  •   
  • Основной путь смазки (оператор не null и execute() выполняется нормально)  
  • выполнить() исключает исключение  
  • foo() выдает запрос исключения AND, возвращаемый как null  Оператор
  • возвращается как null
Якоко указывает 2 ответвления внутри "try" (на нулевой проверке) и 4 внутри finally (на нулевой проверке). Все они полностью покрыты.

Анализ: пример java-7:
Если те же самые 4 теста работают с примером стиля Java7, jacoco указывает, что 6/8 ветвей покрываются (по самой попытке) и 2/2 при нулевой проверке внутри try. Я попробовал несколько дополнительных тестов, чтобы увеличить охват, но я не могу найти способ стать лучше, чем 6/8. Как указывали другие, декомпилированный код (который я также рассматривал) для примера java-7 предполагает, что java-компилятор генерирует недостижимые сегменты для try-with-resource. Jacoco сообщает (точно), что такие сегменты существуют.

Обновление:. Используя стиль кодирования Java7, вы можете получить 100% -ый охват с использованием Java7 JRE (см. ответ Matyas ниже). Однако, используя стиль кодирования Java7 с Java8 JRE, я считаю, что вы попадете в 6/8 ветки. Тот же код, просто другой JRE. Похоже, что байт-код создается по-разному между двумя JRE, когда Java8 создает недостижимые пути.

Ответ 4

Четыре года, но все же...

  • Счастливый путь с ненулевым AutoCloseable
  • Счастливый путь с нулем AutoCloseable
  • Броски на запись
  • Броски на закрытие
  • Броски на запись и закрытие
  • Выдает спецификацию ресурса (вызов части, например, конструктор)
  • Выбрасывает блок try, но AutoCloseable имеет значение null

Выше перечислены все 7 условий - причина для 8 ветвей обусловлена ​​повторным состоянием.

Все ветки могут быть достигнуты, try-with-resources - довольно простой сахар компилятора (по крайней мере, по сравнению с switch-on-string) - если они не могут быть достигнуты, то это по определению ошибка компилятора.

На самом деле требуется всего 6 модульных тестов (в приведенном ниже примере код throwsOnClose равен @Ingore d, а охват ветвей - 8/8.

Также обратите внимание, что Throwable.addSuppressed(Throwable) не может подавить себя, поэтому сгенерированный байт-код содержит дополнительную защиту (IF_ACMPEQ - ссылочное равенство) для предотвращения этого). К счастью, эта ветвь покрывается случаями "бросить-на-писать", "бросить-на-закрывать" и "бросить-на-писать-и-закрыть", так как слоты переменной байт-кода повторно используются внешними 2 из 3 областей обработчика исключений.

Это не проблема с 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)));
        }

    }
}

Eclipse Coverage

Caveat

Хотя не в примере кода OP, есть один случай, который не может быть проверен AFAIK.

Если вы передадите ссылку на ресурс в качестве аргумента, то в Java 7/8 вы должны иметь локальную переменную для назначения:

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

В этом случае сгенерированный код будет по-прежнему защищать ссылку на ресурс. Синтаксический сахар обновлен в Java 9, где локальная переменная больше не требуется: try(arg){ /*...*/ }

Дополнительно - Предложите использование библиотеки, чтобы полностью исключить ветки

По общему признанию, некоторые из этих ветвей можно списывать как нереальные - например, где блок try использует AutoCloseable без нулевой проверки или где ссылка ресурса (with) не может быть нулевой.

Часто ваше приложение не заботится о том, где оно не удалось - открыть файл, записать его или закрыть его - степень детализации неудачна (если приложение не связано конкретно с файлами, например файловым браузером или текстовым процессором).

Кроме того, в OP-коде, чтобы проверить нулевой путь закрытия, вам нужно будет реорганизовать блок try в защищенный метод, подкласс и предоставить реализацию NOOP - все это просто охватит ветки, которые никогда не будут приняты в дикой природе.

Я написал небольшую библиотеку Java 8 io.earcam.unexceptionalMaven Central), который имеет дело с большинством проверенных шаблонов исключений.

В связи с этим вопросом: он предоставляет кучу нуль-ветки, однострочные для AutoCloseable s, конвертированные проверенные исключения в unchecked.

Пример: бесплатный порт Finder

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

Ответ 5

Недавно Джакоко исправил эту проблему, выпуск 0.8.0 (2018/01/02)

"При создании отчетов отфильтровываются различные генерируемые компилятором артефакты, которые в противном случае требуют ненужных, а иногда и невозможных трюков, чтобы не иметь частичного или пропущенного охвата:

  • Часть байт-кода для операторов try-with-resources (GitHub # 500). "

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

Ответ 6

У меня была аналогичная проблема с чем-то вроде этого:

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

он жаловался, что 2 из 8 веток не были охвачены. закончил это:

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

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

никаких других изменений, и я теперь достиг 100%....