web-dev-qa-db-fra.com

8 succursales pour essayer avec des ressources - couverture jacoco possible?

J'ai du code qui utilise try avec des ressources et en jacoco, il est à peine couvert. Toutes les lignes de code source sont vertes, mais je reçois un petit symbole jaune me disant que seules 4 des 8 branches sont couvertes.

enter image description here

J'ai du mal à comprendre quelles sont toutes les branches et comment écrire du code qui les couvre. Trois emplacements possibles jettent PipelineException. Ce sont createStageList(), processItem() et les implicites close()

  1. Ne levant aucune exception,
  2. levée d'une exception depuis createStageList()
  3. levée d'une exception depuis processItem()
  4. levée d'une exception depuis close()
  5. levée d'une exception depuis processItem() et close()

Je ne peux penser à aucun autre cas, mais je n'ai encore que 4 des 8 couverts.

Quelqu'un peut-il m'expliquer pourquoi c'est 4 sur 8 et est-il possible de toucher les 8 branches? Je ne suis pas doué pour décrypter/lire/interpréter le code d'octet, mais peut-être que vous l'êtes ... :) J'ai déjà vu https://github.com/jacoco/jacoco/issues/82 , mais ni lui ni le problème auquel il fait référence aident beaucoup (à part noter que cela est dû aux blocs générés par le compilateur)

Hmm, juste au moment où j'ai fini d'écrire ceci, j'ai pensé à quels cas pourraient ne pas être testés par ce que je mentionne ci-dessus ... Je posterai une réponse si j'ai bien compris. Je suis sûr que cette question et sa réponse aideront quelqu'un dans tous les cas.

EDIT: Non, je ne l'ai pas trouvé. Lancer des RuntimeExceptions (non gérées par le bloc catch) ne couvrait plus de branches

61
Gus

Eh bien, je ne peux pas vous dire quel est le problème exact avec Jacoco, mais je peux vous montrer comment est compilé Try With Resources. Fondamentalement, il existe de nombreux commutateurs générés par le compilateur pour gérer les exceptions levées à divers points.

Si nous prenons le code suivant et le compilons

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

Et puis démonter, nous obtenons

.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

Pour ceux qui ne parlent pas de bytecode, cela équivaut à peu près au pseudo Java suivant. J'ai dû utiliser gotos car le bytecode ne correspond pas vraiment au flux de contrôle Java.

Comme vous pouvez le voir, il existe de nombreux cas pour gérer les différentes possibilités d'exceptions supprimées. Il n'est pas raisonnable de pouvoir couvrir tous ces cas. En fait, le goto L59 la branche du premier bloc try est impossible à atteindre, car le premier catch Throwable interceptera toutes les exceptions.

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

enter image description here

Je peux couvrir les 8 branches, donc ma réponse est OUI. Regardez le code suivant, ce n'est qu'un essai rapide, mais cela fonctionne (ou consultez mon github: https://github.com/bachoreczm/basicjava et le package 'trywithresources', là vous pouvez trouver, comment fonctionne try-with-resources, voir la classe '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;
  }
}
8

Pas de vraie question, mais je voulais lancer plus de recherches. tl; dr = Il semble que vous pouvez atteindre une couverture de 100% pour essayer enfin, mais pas pour essayer avec ressource.

Naturellement, il y a une différence entre la version d'essai de la vieille école et la version d'essai de Java7 avec des ressources. Voici deux exemples équivalents montrant la même chose en utilisant des approches alternatives.

Exemple Old School (une approche d'essai):

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

Exemple Java7 (une approche d'essayer avec des ressources):

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

Analyse: exemple old-school:
En utilisant Jacoco 0.7.4.201502262128 et JDK 1.8.0_45, j'ai pu obtenir 100% de couverture de ligne, d'instruction et de branche sur l'exemple Old School en utilisant les 4 tests suivants:

  • Chemin de graissage de base (l'instruction n'est pas nulle et execute () s'exerce normalement)
  • execute () lève une exception
  • foo () lève une exception ET l'instruction retournée comme nulle
  • instruction retournée comme nulle

Analyse: exemple Java-7:
Si les 4 mêmes tests sont exécutés contre l'exemple de style Java7, jacoco indique que 6/8 branches sont couvertes (lors de l'essai lui-même) et 2/2 lors de la vérification nulle dans l'essai. J'ai essayé un certain nombre de tests supplémentaires pour augmenter la couverture, mais je ne trouve aucun moyen de faire mieux que 6/8. Comme d'autres l'ont indiqué, le code décompilé (que j'ai également examiné) pour l'exemple Java-7 suggère que le compilateur Java génère des segments inaccessibles pour les essais avec ressource. Jacoco rapporte (avec précision) que de tels segments existent.

Mise à jour: En utilisant le style de codage Java7, vous pourrez peut-être obtenir une couverture de 100% [~ # ~] si [~ # ~] en utilisant un Java7 JRE (voir la réponse de Matyas ci-dessous). Cependant, en utilisant le style de codage Java7 avec un JRE Java8, je pense que vous atteindrez les 6/8 branches couvertes. Même code, juste différent JRE. Il semble que le code d'octet soit créé différemment entre les deux JRE, celui de Java8 créant des chemins inaccessibles.

6
Jeff Bennett

Quatre ans, mais quand même ...

  1. Chemin heureux avec non nul AutoCloseable
  2. Chemin heureux avec null AutoCloseable
  3. Jette sur l'écriture
  4. Jette sur fermer
  5. Lance l'écriture et la fermeture
  6. Lance la spécification des ressources (la partie avec, par exemple appel constructeur)
  7. Lance dans le bloc try mais AutoCloseable est nul

Ci-dessus énumère les 7 conditions - la raison des 8 branches est due à une condition répétée.

Toutes les branches sont accessibles, le try-with-resources Est un sucre de compilation assez simple (au moins par rapport à switch-on-string) - si elles ne peuvent pas être atteintes, alors c'est par définition un bug du compilateur.

Seuls 6 tests unitaires sont réellement requis (dans l'exemple de code ci-dessous, throwsOnClose est @Ingore D et la couverture des succursales est 8/8.

Notez également que Throwable.addSuppressed (Throwable) ne peut pas se supprimer lui-même, donc le bytecode généré contient un garde supplémentaire (IF_ACMPEQ - égalité de référence) pour éviter cela). Heureusement, cette branche est couverte par les cas de projection sur écriture, de fermeture sur fermeture et de projection sur écriture et fermeture, car les emplacements de variable de bytecode sont réutilisés par les régions externes du gestionnaire d'exceptions 2 sur 3.

Ce n'est pas un problème avec Jacoco - en fait, l'exemple de code dans le lien problème # 82 est incorrect car il n'y a pas vérifications nulles dupliquées et il n'y a pas de bloc de capture imbriqué entourant la fermeture.

Test JUnit démontrant 8 des 8 branches couvertes

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

Bien qu'il ne soit pas dans l'exemple de code OP, il y a un cas qui ne peut pas être testé AFAIK.

Si vous passez la référence de ressource comme argument, alors dans Java 7/8, vous devez avoir une variable locale à affecter à:

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

Dans ce cas, le code généré gardera toujours la référence de la ressource. Le sucre syntaxique est mis à jour dans Java 9 , où la variable locale n'est plus requise: try(arg){ /*...*/ }

Supplémentaire - Suggère l'utilisation de la bibliothèque pour éviter complètement les branches

Certes, certaines de ces branches peuvent être considérées comme irréalistes - c'est-à-dire où le bloc try utilise le AutoCloseable sans vérification nulle ou où la référence de ressource (with) ne peut pas être nulle.

Souvent votre application ne se soucie pas de l'endroit où elle a échoué - pour ouvrir le fichier, y écrire ou le fermer - la granularité de l'échec n'est pas pertinente (sauf si l'application est spécifiquement concernée par les fichiers, par exemple le fichier - navigateur ou traitement de texte).

De plus, dans le code de l'OP, pour tester le chemin fermable nul - vous devez refactoriser le bloc try dans une méthode protégée, sous-classe et fournir une implémentation NOOP - tout cela ne fait que couvrir les branches qui ne seront jamais prises à l'état sauvage .

J'ai écrit une minuscule bibliothèque Java 8 io.earcam.unexceptional (in Maven Central )) qui traite de la plupart des passe-partout d'exceptions vérifiés.

Relatif à cette question: il fournit un tas de lignes uniques à branche zéro pour AutoCloseable, convertissant les exceptions vérifiées en non vérifiées.

Exemple: Free Port Finder

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

Jacoco a récemment résolu ce problème, version 0.8.0 (2018/01/02)

"Lors de la création des rapports, divers artefacts générés par le compilateur sont filtrés, ce qui nécessite autrement des astuces inutiles et parfois impossibles pour ne pas avoir une couverture partielle ou manquée:

  • Une partie du bytecode pour les instructions try-with-resources (GitHub # 500). "

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

2
John Bedalov

j'ai eu un problème similaire avec quelque chose comme ça:

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

il s'est plaint que 2 des 8 succursales n'étaient pas couvertes. a fini par faire ceci:

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

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

aucun autre changement et j'ai maintenant atteint 100% ....

1
mdeanda