8 twigs pour essayer avec des ressources – couverture jacoco possible?

J’ai du code qui utilise try avec des ressources et en jacoco, il est à moitié couvert. Toutes les lignes de code source sont vertes, mais un petit symbole jaune me dit que seules 4 des 8 twigs sont couvertes.

entrer la description de l'image ici

J’ai du mal à déterminer quelles sont toutes les twigs et comment écrire du code pour les couvrir. Trois endroits possibles jettent PipelineException . Ce sont createStageList() , processItem() et la close() implicite close()

  1. Ne jetant aucune exception,
  2. lancer une exception à partir de createStageList()
  3. lancer une exception à partir de processItem()
  4. lancer une exception de close()
  5. lancer une exception à partir de processItem() et close()

Je ne peux penser à aucun autre cas, pourtant je n’ai toujours que 4 sur 8 couverts.

Est-ce que quelqu’un peut m’expliquer pourquoi il est 4 sur 8 et est-ce qu’il y a de toute façon pour bash les 8 twigs? Je ne suis pas habile à déchiffrer / lire / interpréter le code octet, mais peut-être que vous êtes … 🙂 J’ai déjà vu https://github.com/jacoco/jacoco/issues/82 , mais ni lui ni le problème il fait beaucoup appel à l’aide (autre que de 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 réfléchi sur le ou les cas qui ne seraient pas 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 RuntimeExceptions (non géré par le bloc catch) ne couvre plus les twigs

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

    Si nous prenons le code suivant et le compilons

     public static void main(Ssortingng[] args){ Ssortingng a = "before"; try (CharArrayWriter br = new CharArrayWriter()) { br.writeTo(null); } catch (IOException e){ System.out.println(e.getMessage()); } Ssortingng a2 = "after"; } 

    Et puis démonter, nous obtenons

     .method static public main : ([Ljava/lang/Ssortingng;)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  ()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/Ssortingng; Object java/lang/Ssortingng 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/Ssortingng; Object java/lang/Ssortingng 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/Ssortingng; Object java/lang/Ssortingng 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/Ssortingng; invokevirtual java/io/PrintStream println (Ljava/lang/Ssortingng;)V L108: .stack same ldc 'after' astore_2 return .end method 

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

    Comme vous pouvez le constater, 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, il est impossible d’atteindre la twig goto L59 sur le premier bloc d’essai, car la première capture Throwable intercepte 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) } 

    entrer la description de l'image ici

    Je peux couvrir les 8 twigs, donc ma réponse est OUI. Regardez le code suivant, ceci est seulement un essai rapide, mais ça marche (ou voyez mon github: https://github.com/bachoreczm/basicjava et le paquet «trywithresources», vous pouvez trouver, comment ressources fonctionne, 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; } } 

    Pas de vraie question, mais je voulais lancer plus de recherches. tl; dr = Il semble que vous puissiez atteindre 100% de couverture pour try-finally, mais pas pour try-with-resource.

    Naturellement, il y a une différence entre les anciennes méthodes d’essayage et Java7. Voici deux exemples équivalents montrant la même chose en utilisant des approches alternatives.

    Exemple Old School (approche try-finally):

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

    Exemple Java7 (une approche try-with-resource):

     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 une couverture de ligne, d’instruction et de twig de 100% dans l’exemple Old School en utilisant les 4 tests suivants:

    • Chemin de graissage de base (instruction not null et execute () s’exerce normalement)
    • execute () lève une exception
    • foo () renvoie une instruction AND exception retournée sous la forme null
    • déclaration retournée comme nulle

    Jacoco indique 2 twigs à l’intérieur du ‘try’ (sur la vérification NULL) et 4 à l’intérieur du final (sur la vérification NULL). Tous sont entièrement couverts.

    Analyse: exemple java-7:
    Si les mêmes 4 tests s’exécutent sur l’exemple de style Java7, jacoco indique que 6/8 twigs sont couvertes (sur le test même) et 2/2 sur le null-check dans try. 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é) de l’exemple java-7 suggère que le compilateur Java génère des segments inaccessibles pour try-with-resource. Jacoco rapporte (avec précision) que de tels segments existent.

    Mise à jour: en utilisant le style de codage Java7, vous pouvez obtenir une couverture de 100% si vous utilisez un JRE Java7 (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 twigs couvertes. Même code, juste différent JRE. On dirait que le code d’octet est créé différemment entre les deux JRE avec celui de Java8 créant des chemins inaccessibles.

    Quatre ans, mais quand même …

    1. Chemin heureux avec AutoCloseable non nul
    2. Chemin heureux avec null AutoCloseable
    3. Jette par écrit
    4. Jette à proximité
    5. Jette à l’écriture et ferme
    6. Jette dans la spécification de ressource (la partie avec , par exemple, l’appel de constructeur)
    7. AutoCloseable bloc try mais AutoCloseable est nul

    La liste ci-dessus répertorie les 7 conditions – la raison pour laquelle les 8 twigs sont dues à des conditions répétées.

    Toutes les twigs peuvent être atteintes, le try-with-resources est assez simple (au moins comparé à switch-on-ssortingng ) – si elles ne peuvent être atteintes, il s’agit par définition d’un bogue de compilation.

    Seulement 6 tests unitaires sont réellement requirejs (dans l’exemple de code ci-dessous, throwsOnClose est @Ingore d et la couverture de twig est 8/8.

    Notez également que Throwable.addSuppressed (Throwable) ne peut pas se supprimer, de sorte que le bytecode généré contient une protection supplémentaire (IF_ACMPEQ – égalité de référence) pour empêcher cela). Heureusement, cette twig est couverte par les scénarios de lancement, de fermeture et de réécriture, car les emplacements de variable bytecode sont réutilisés par les 2 zones externes de gestionnaire d’exceptions.

    Ce n’est pas un problème avec Jacoco – en fait, l’exemple de code dans le problème lié n ° 82 est incorrect car il n’y a pas de vérifications NULL dupliquées et il n’y a pas de bloc catch nested entourant la clôture.

    JUnit test démontrant 8 des 8 twigs 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(Ssortingng 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 twigs */ @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 twigs */ @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 twigs */ @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))); } } } 

    Couverture de l'éclipse

    Caveat

    Bien que n’étant pas dans le code exemple d’OP, il existe un cas qui ne peut pas être testé sur AFAIK.

    Si vous transmettez la référence de ressource en tant qu’argument, vous devez avoir une variable locale à affecter à Java 7/8:

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

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

    Supplemental – Suggère l’utilisation de la bibliothèque pour éviter les twigs entièrement

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

    Souvent, votre application ne se soucie pas de savoir où elle a échoué – pour ouvrir le fichier, y écrire ou la fermer – la granularité de l’échec est sans importance (sauf si l’application concerne spécifiquement des fichiers, par exemple un navigateur de fichiers ou un traitement de texte).

    De plus, dans le code de l’OP, pour tester le chemin NULL closable – vous devrez refactoriser le bloc try en une méthode protégée, une sous-classe et fournir une implémentation NOOP – tout cela ne fera que couvrir les twigs qui ne seront jamais utilisées .

    J’ai écrit une minuscule bibliothèque Java 8 io.earcam.unexceptional (dans Maven Central ) qui traite de la plupart des exceptions vérifiées.

    Pertinent pour cette question: il fournit un tas de one-liners à zéro twig pour AutoCloseable , convertissant les exceptions vérifiées en non cochées.

    Exemple: Finder Free Port

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

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

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

    • Partie du bytecode pour les instructions try-with-resources (GitHub # 500). ”

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

    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 twigs n’étaient pas couvertes. a fini par faire ça:

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

    aucun autre changement et j’ai maintenant atteint 100% ….