it-swarm.com.de

8 Filialen zum Ausprobieren - Jacoco-Abdeckung möglich?

Ich habe einen Code, der try with resources verwendet, und in jacoco wird er nur zur Hälfte behandelt. Alle Quellcodezeilen sind grün, aber ich erhalte ein kleines gelbes Symbol, das mich darauf hinweist, dass nur 4 von 8 Zweigen abgedeckt sind.

enter image description here

Ich habe Probleme herauszufinden, was alle Zweige sind und wie man Code schreibt, der sie abdeckt. Drei mögliche Orte werfen PipelineException. Dies sind createStageList(), processItem() und die implizierte close()

  1. Keine Ausnahmen werfen,
  2. eine Ausnahme von createStageList() auslösen
  3. eine Ausnahme von processItem() auslösen
  4. eine Ausnahme von close() auslösen
  5. eine Ausnahme von processItem() und close() auslösen

Ich kann mir keine anderen Fälle vorstellen, aber ich habe immer noch nur 4 von 8 Fällen.

Kann mir jemand erklären, warum es 4 von 8 ist und gibt es trotzdem alle 8 Zweige zu treffen? Ich bin nicht in der Lage, Bytecode zu entschlüsseln/zu lesen/zu interpretieren, aber vielleicht sind Sie es ... :) Ich habe es bereits gesehen https://github.com/jacoco/jacoco/issues/82 , aber weder es noch das Problem, auf das es verweist, helfen sehr (abgesehen davon, dass dies auf vom Compiler generierte Blöcke zurückzuführen ist)

Hmm, gerade als ich mit dem Schreiben fertig war, hatte ich einen Gedanken darüber, welche Fälle möglicherweise nicht mit den oben genannten getestet wurden ... Ich werde eine Antwort posten, wenn ich es richtig verstanden habe. Ich bin mir sicher, dass diese Frage und ihre Antwort auf jeden Fall jemandem helfen werden.

EDIT: Nein, ich habe es nicht gefunden. Das Auslösen von RuntimeExceptions (nicht vom catch-Block behandelt) deckte keine weiteren Verzweigungen ab

61
Gus

Nun, ich kann Ihnen nicht sagen, was genau das Problem mit Jacoco ist, aber ich kann Ihnen zeigen, wie Try With Resources kompiliert wird. Grundsätzlich gibt es viele vom Compiler generierte Schalter, um Ausnahmen zu behandeln, die an verschiedenen Punkten ausgelöst werden.

Wenn wir den folgenden Code nehmen und kompilieren

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

Und dann zerlegen wir bekommen

.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

Für diejenigen, die keinen Bytecode sprechen, entspricht dies in etwa dem folgenden Pseudo-Java. Ich musste gotos verwenden, da der Bytecode nicht wirklich dem Kontrollfluss von Java entspricht.

Wie Sie sehen, gibt es viele Fälle, in denen die verschiedenen Möglichkeiten unterdrückter Ausnahmen behandelt werden müssen. Es ist nicht sinnvoll, alle diese Fälle abdecken zu können. In der Tat ist die goto L59 Die Verzweigung im ersten try-Block ist nicht zu erreichen, da der erste catch Throwable alle Ausnahmen abfängt.

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

Ich kann alle 8 Zweige abdecken, also lautet meine Antwort JA. Schauen Sie sich den folgenden Code an, dies ist nur ein schneller Versuch, aber er funktioniert (oder sehen Sie sich meinen Github an: https://github.com/bachoreczm/basicjava und das Paket 'trywithresources', da können Sie finden Sie, wie try-with-resources funktioniert (siehe Klasse '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

Keine wirkliche Frage, aber ich wollte mehr Nachforschungen anstellen. tl; dr = Es sieht so aus, als könnten Sie eine 100% ige Abdeckung für try-finally erzielen, nicht jedoch für try-with-resource.

Verständlicherweise gibt es einen Unterschied zwischen "try-finally" der alten Schule und "try-with-resources" von Java7. Hier sind zwei äquivalente Beispiele, die dasselbe anhand alternativer Ansätze zeigen.

Old School-Beispiel (ein Try-finally-Ansatz):

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

Java7-Beispiel (ein Ansatz zum Ausprobieren von Ressourcen):

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

Analyse: Old-School-Beispiel:
Mit Jacoco 0.7.4.201502262128 und JDK 1.8.0_45 konnte ich anhand der folgenden 4 Tests eine 100% ige Linien-, Anweisungs- und Zweigabdeckung für das Old-School-Beispiel erzielen:

  • Grundlegender Fettpfad (Anweisung nicht null und execute () wird normal ausgeführt)
  • execute () löst eine Ausnahme aus
  • foo () löst eine Ausnahme aus UND-Anweisung, die als null zurückgegeben wird
  • anweisung wird als null zurückgegeben

Analyse: Java-7 Beispiel:
Wenn dieselben 4 Tests im Java7-Stil ausgeführt werden, gibt jacoco an, dass 6/8 Zweige (beim Versuch selbst) und 2/2 beim Null-Check innerhalb des Versuchs abgedeckt sind. Ich habe eine Reihe zusätzlicher Tests versucht, um die Abdeckung zu erhöhen, aber ich kann keinen Weg finden, um besser als 6/8 zu werden. Wie andere bereits angedeutet haben, deutet der dekompilierte Code (den ich mir auch angeschaut habe) für das Java-7-Beispiel darauf hin, dass der Java= Compiler nicht erreichbare Segmente für den Test mit Ressourcen generiert. Jacoco meldet dies (genau), dass solche Segmente existieren.

pdate: Wenn Sie den Java7-Codierungsstil verwenden, können Sie möglicherweise eine 100-prozentige Abdeckung erhalten [~ # ~], wenn [~ # ~] mit einer Java7 JRE (siehe Matyas Antwort unten). Wenn Sie den Java7-Codierungsstil mit einer Java8-JRE verwenden, werden Sie meines Erachtens die 6/8-Zweige erreichen, die abgedeckt werden. Gleicher Code, nur andere JRE. Es scheint, als würde der Bytecode zwischen den beiden JREs unterschiedlich erstellt, wobei der Java8-Code nicht erreichbare Pfade erzeugt.

6
Jeff Bennett

Vier Jahre alt, aber immer noch ...

  1. Glücklicher Pfad mit Nicht-Null AutoCloseable
  2. Glücklicher Pfad mit null AutoCloseable
  3. Wirft auf zu schreiben
  4. Wirft auf zu schließen
  5. Throws on write und close
  6. Eingaben in die Ressourcenspezifikation ( mit Teil, z. Konstruktoraufruf)
  7. Löst den Block try aus, aber AutoCloseable ist null

Oben sind alle 7 Zustände aufgelistet - der Grund für die 8 Zweige ist ein wiederholter Zustand.

Alle Zweige sind erreichbar, der try-with-resources Ist ein ziemlich einfacher Compilerzucker (zumindest im Vergleich zu switch-on-string) - wenn sie nicht erreichbar sind, handelt es sich per Definition um einen Compilerfehler.

Tatsächlich sind nur 6 Komponententests erforderlich (im folgenden Beispielcode ist throwsOnClose@Ingore D und die Zweigabdeckung beträgt 8/8.

Beachten Sie auch, dass Throwable.addSuppressed (Throwable) sich nicht selbst unterdrücken kann, sodass der generierte Bytecode einen zusätzlichen Guard enthält (IF_ACMPEQ - Referenzgleichheit), um dies zu verhindern. Glücklicherweise wird diese Verzweigung durch die Fälle "Throw-On-Write", "Throw-On-Close" und "Throw-On-Write-And-Close" abgedeckt, da die Bytecode-Variablen-Slots von den äußeren 2 von 3 Ausnahmebehandlungsbereichen wiederverwendet werden.

Dies ist kein Problem mit Jacoco - tatsächlich ist der Beispielcode im verlinkten Problem # 82 falsch, da es keine gibt doppelte Nullprüfungen und es gibt keinen verschachtelten Fangblock, der den Abschluss umgibt.

JUnit-Test mit 8 von 8 abgedeckten Zweigen

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

Vorbehalt

Obwohl nicht in OPs Beispielcode, gibt es einen Fall, der nicht AFAIK getestet werden kann.

Wenn Sie die Ressourcenreferenz als Argument übergeben, müssen Sie in Java 7/8) eine lokale Variable haben, der Folgendes zugewiesen werden kann:

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

In diesem Fall schützt der generierte Code weiterhin die Ressourcenreferenz. Der syntatische Zucker ist aktualisiert in Java 9 , wobei die lokale Variable nicht mehr benötigt wird: try(arg){ /*...*/ }

Ergänzend - Schlagen Sie die Verwendung der Bibliothek vor, um Verzweigungen vollständig zu vermeiden

Zugegebenermaßen können einige dieser Zweige als unrealistisch abgeschrieben werden - d. H., Wenn der try-Block den AutoCloseable ohne Nullprüfung verwendet oder wenn die Ressourcenreferenz (with) nicht null sein kann.

Häufig Ihre Anwendung ist es egal, wo es fehlgeschlagen ist - die Datei zu öffnen, zu schreiben oder zu schließen - die Granularität des Fehlers ist irrelevant (es sei denn, die App befasst sich speziell mit Dateien, z. B. Datei -Browser oder Textverarbeitung).

Darüber hinaus müssen Sie im OP-Code den Try-Block in eine geschützte Methode umwandeln, eine Unterklasse erstellen und eine NOOP-Implementierung bereitstellen, um den Pfad zu testen, der nicht geschlossen werden kann .

Ich habe eine winzige Java 8 Bibliothek io.earcam.unexceptional (in Maven Central ) geschrieben, die sich mit den meisten geprüften Ausnahmen befasst.

Relevant für diese Frage: Es enthält eine Reihe von Einzeiler-Zweigen ohne Verzweigung für AutoCloseables, die markierte Ausnahmen in nicht markierte konvertieren.

Beispiel: Free Port Finder

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

Jacoco hat dieses Problem kürzlich behoben, Release 0.8.0 (01.01.2018)

"Während der Erstellung von Berichten werden verschiedene vom Compiler erzeugte Artefakte herausgefiltert, die ansonsten unnötige und manchmal unmögliche Tricks erfordern, um keine teilweise oder verpasste Abdeckung zu haben:

  • Teil des Bytecodes für Try-with-Resources-Anweisungen (GitHub # 500). "

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

2
John Bedalov

ich hatte ein ähnliches Problem mit so etwas:

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

es wurde beanstandet, dass 2 von 8 Filialen nicht abgedeckt waren. endete damit:

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

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

keine weiteren Änderungen und ich habe jetzt 100% erreicht ....

1
mdeanda