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


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

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

  1. не бросая любые исключения,
  2. исключение из createStageList()
  3. исключение из processItem()
  4. исключение из close()
  5. исключение из processItem() и close()

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

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

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

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

6 60

6 ответов:

Ну я не могу сказать вам, что именно проблема с Jacoco, но я могу показать вам, как Try с ресурсами компилируется. В принципе, существует множество генерируемых компилятором коммутаторов для обработки исключений, возникающих в различных точках.

если мы возьмем следующий код и скомпилируем его

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 ветвь с первой попытки блока невозможно достичь, так как первый 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)
}

enter image description here

Я могу охватить все 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;
  }
}

нет реального вопроса, но хотел бросить больше исследований там. 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 теста:

  • Basic grease path (оператор not null, а execute () выполняется нормально)
  • execute () бросает исключение
  • foo () выбрасывает исключение и оператор возвращается как null
  • оператор возвращается как null
Jacoco указывает на 2 ветви внутри " try " (на нулевой проверке) и 4 внутри finally (на нулевой проверке). Все они покрыты полностью.

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

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

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

  1. счастливый путь с ненулевым AutoCloseable
  2. счастливый путь с null AutoCloseable
  3. бросает на запись
  4. бросает на близких
  5. выбрасывает на запись и закрыть!--19-->
  6. бросает в спецификации ресурса (С часть, например, вызов конструктора)
  7. бросает в try блок а AutoCloseable равно null

Выше перечислены все 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-close, поскольку слоты переменных байт-кода повторно используются внешними 2 из 3 областей обработчика исключений.

это не проблема с Jacoco-на самом деле пример кода в связанном выпуск #82 неверно, так как нет дублированного null проверяет и нет вложенного блока 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

будьте осторожны

хотя и не в примере кода 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.заурядный (in Maven Central), который имеет дело с наиболее проверенным шаблоном исключения.

относится к этому вопросу: он предоставляет кучу нулевой ветви, однострочные для AutoCloseables, преобразование проверенных исключений в непроверенные.

Пример: Free Port Finder

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

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

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

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

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

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

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

он жаловался, что 2 из 8 ветвей не были покрыты. в итоге получилось так:

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

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

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