java plugin - 8 sucursales para probar con recursos: ¿es posible la cobertura de jacoco?





jar pruebas (6)


Jacoco resolvió recientemente este problema, Release 0.8.0 (2018/01/02)

"Durante la creación de informes, varios artefactos generados por el compilador se filtran, lo que de otro modo requiere trucos innecesarios ya veces imposibles para no tener cobertura parcial o perdida:

  • Parte de bytecode para declaraciones try-with-resources (GitHub # 500). "

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

Tengo un código que usa try with resources y en jacoco está apareciendo medio cubierto. Todas las líneas de código fuente son verdes, pero obtengo un pequeño símbolo amarillo que me dice que solo 4 de 8 ramas están cubiertas.

Tengo problemas para averiguar qué son todas las ramas y cómo escribir el código que las cubre. Tres posibles lugares lanzan PipelineException . Estos son createStageList() , processItem() y el implied close()

  1. No lanzando ninguna excepción,
  2. lanzando una excepción desde createStageList()
  3. lanzando una excepción desde processItem()
  4. lanzando una excepción desde close()
  5. lanzando una excepción desde processItem() y close()

No puedo pensar en ningún otro caso, sin embargo, solo tengo 4 de 8 cubiertos.

¿Puede alguien explicarme por qué son 4 de 8 y de todos modos hay disponible para golpear las 8 ramas? No soy experto en descifrar / leer / interpretar el código de bytes, pero tal vez tú ... :) Ya he visto https://github.com/jacoco/jacoco/issues/82 , pero ni el problema ni el problema hace referencia a la ayuda (aparte de señalar que esto se debe a bloques generados por el compilador)

Hmm, justo cuando termino de escribir esto, pensé en qué caso (s) podría no estar probado por lo que mencioné anteriormente ... Publicaré una respuesta si lo hice bien. Estoy seguro de que esta pregunta y su respuesta ayudarán a alguien en cualquier caso.

EDITAR: No, no lo encontré. Lanzando RuntimeExceptions (no manejado por el bloque catch) no cubría más ramas




Puedo cubrir las 8 ramas, por lo que mi respuesta es SÍ. Mire el siguiente código, este es solo un intento rápido, pero funciona (o vea mi github: https://github.com/bachoreczm/basicjava y el paquete 'trywithresources', allí puede encontrar, cómo probar-con- los recursos funcionan, consulte la clase '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;
  }
}



Bueno, no puedo decirte cuál es el problema exacto con Jacoco, pero puedo mostrarte cómo se compila Try With Resources. Básicamente, hay muchos modificadores generados por el compilador para manejar excepciones lanzadas en varios puntos.

Si tomamos el siguiente código y lo compilamos

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

Y luego desmontar, obtenemos

.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

Para aquellos que no hablan bytecode, esto es más o menos equivalente al siguiente pseudo Java. Tuve que usar gotos porque el bytecode no corresponde realmente al flujo de control de Java.

Como puede ver, hay muchos casos para manejar las diversas posibilidades de excepciones suprimidas. No es razonable poder cubrir todos estos casos. De hecho, la rama goto L59 en el primer bloque try es imposible de alcanzar, ya que la primera captura Throwable capturará todas las excepciones.

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



Cuatro años, pero aún ...

  1. Feliz camino con AutoCloseable no nulo
  2. Feliz camino con nulo AutoCloseable
  3. Lanza en escribir
  4. Lanza cerca
  5. Lanza sobre escribir y cerrar
  6. Lanza en especificación de recursos (la parte con , por ejemplo, llamada de constructor)
  7. Lanza en bloque try pero AutoCloseable es nulo

Arriba enumera las 7 condiciones: la razón de las 8 ramas se debe a la repetición de las condiciones.

Se puede acceder a todas las sucursales, try-with-resources es bastante simple compilador de azúcar (al menos en comparación con switch-on-string ) - si no pueden ser alcanzados, entonces es por definición un error de compilación.

Solo se requieren 6 pruebas unitarias (en el siguiente código de ejemplo, throwsOnClose is @Ingore d y la cobertura de sucursales es 8/8.

También tenga en cuenta que Throwable.addSuppressed(Throwable) no puede suprimirse, por lo que el bytecode generado contiene una protección adicional (IF_ACMPEQ - igualdad de referencia) para evitar esto). Afortunadamente, esta rama está cubierta por los casos throw-on-write, throw-on-close y throw-on-write-and-close, ya que las ranuras variables bytecode son reutilizadas por las regiones externas 2 o 3 del manejador de excepciones.

Esto no es un problema con Jacoco; de hecho, el código de ejemplo en el https://github.com/jacoco/jacoco/issues/82 vinculado https://github.com/jacoco/jacoco/issues/82 es incorrecto, ya que no hay controles nulos duplicados y no hay un bloque de captura anidado que rodee el cierre.

Prueba JUnit que demuestra 8 de 8 ramas cubiertas

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

    }
}

Advertencia

Aunque no está en el código de muestra de OP, hay un caso que no se puede probar AFAIK.

Si pasa la referencia de recurso como argumento, entonces en Java 7/8 debe tener una variable local para asignar a:

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

En este caso, el código generado seguirá protegiendo la referencia del recurso. El azúcar sintáctico se actualiza en Java 9 , donde la variable local ya no es necesaria: try(arg){ /*...*/ }

Complementario: sugiera el uso de la biblioteca para evitar las ramas por completo

Hay que admitir que algunas de estas ramas se pueden descartar como no realistas, es decir, donde el bloque try usa AutoCloseable sin verificación nula o donde la referencia del recurso ( with ) no puede ser nula.

A menudo, a su aplicación no le importa dónde falló: para abrir el archivo, escribir en él o cerrarlo, la granularidad de la falla es irrelevante (a menos que la aplicación esté específicamente relacionada con los archivos, por ejemplo, el buscador de archivos o el procesador de textos).

Además, en el código de OP, para probar el camino de cierre nulo, tendrías que refactorizar el bloque de prueba en un método protegido, subclase y proporcionar una implementación de NOOP, todo esto solo obtendría cobertura en ramas que nunca se tomarán en la naturaleza. .

Escribí una pequeña biblioteca de Java 8 io.earcam.unexceptional (en Maven Central ) que se ocupa de la mayoría de las repeticiones repetidas .

Pertinente a esta pregunta: proporciona un grupo de ramas cero y de una sola línea para AutoCloseable , convirtiendo las excepciones marcadas en desmarcadas.

Ejemplo: Buscador de puertos gratis

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



No es una pregunta real, pero quería lanzar más investigación por ahí. tl; dr = Parece que puedes lograr una cobertura del 100% para try-finally, pero no para try-with-resource.

Es comprensible que exista una diferencia entre try-finally de la vieja escuela y try-with-resources de Java7. Aquí hay dos ejemplos equivalentes que muestran lo mismo utilizando enfoques alternativos.

Ejemplo de Old School (un enfoque try-finally):

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

Ejemplo de Java7 (un enfoque try-with-resource):

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

Análisis: ejemplo de la vieja escuela:
Usando Jacoco 0.7.4.201502262128 y JDK 1.8.0_45, pude obtener el 100% de línea, instrucción y cobertura de sucursal en el ejemplo de Old School usando las siguientes 4 pruebas:

  • Ruta básica de la grasa (la declaración no es nula y execute () se ejecuta normalmente)
  • execute () arroja una excepción
  • foo () arroja la excepción Y la declaración devuelta como nula
  • declaración devuelta como nula
Jacoco indica 2 ramas dentro del "intento" (en la verificación nula) y 4 dentro del final (en la verificación nula). Todos están cubiertos completamente.

Análisis: ejemplo de java-7:
Si las mismas 4 pruebas se ejecutan en el ejemplo del estilo Java7, jacoco indica que se cubren 6/8 ramas (en la prueba en sí) y 2/2 en la verificación nula dentro del intento. Probé varias pruebas adicionales para aumentar la cobertura, pero no puedo encontrar una forma de mejorarme de 6/8. Como otros han indicado, el código decompilado (que también vi) para el ejemplo java-7 sugiere que el compilador Java está generando segmentos inalcanzables para try-with-resource. Jacoco informa (con precisión) que tales segmentos existen.

Actualización: al utilizar el estilo de codificación Java7, es posible que pueda obtener una cobertura del 100% SI utiliza un Java7 JRE (consulte la respuesta de Matyas a continuación). Sin embargo, usando el estilo de codificación Java7 con Java8 JRE, creo que alcanzará las 6/8 ramas cubiertas. Mismo código, solo JRE diferente. Parece que el código de bytes se está creando de forma diferente entre los dos JRE con el Java8 creando rutas inalcanzables.




Mi estilo preferido es colocarlos donde parezcan tener más sentido. Por lo general, esto está en la parte inferior, por lo que están fuera del camino, pero a veces me parece que tiene más sentido ponerlos antes de cierto grupo de métodos (si estos son los métodos que usan la clase interna).

Sin embargo, si la clase se vuelve demasiado pesada con muchos métodos y clases internas, probablemente sea una mala elección de diseño (la cohesión es demasiado baja). A veces dejo que las clases lleguen de esta manera por accidente y son horribles de tratar más adelante. Si puedo ver uno yendo de esa manera, generalmente lo refactorizaré, tal vez incluso en su propio paquete. Si llegas al punto en el que tienes tantas clases internas que no sabes qué hacer con ellas, tomaría este enfoque. Incluso hay algunos que desaconsejan el uso de clases internas por este motivo (aunque no estoy de acuerdo, son un recurso valioso cuando se usan de forma adecuada, solo hay que tener cuidado de que no se salgan de control).





java code-coverage bytecode jacoco try-with-resources