java - 8 فروع لمحاولة مع الموارد-تغطية jacoco ممكن؟



code-coverage bytecode try-with-resources (6)

حسناً ، لا أستطيع أن أخبركم ما هي المشكلة بالضبط مع Jacoco ، ولكن يمكنني أن أريكم كيف يتم تجميع Try With Resources. أساسا ، هناك الكثير من مفاتيح التحويل البرمجي التي تم إنشاؤها للتعامل مع الاستثناءات التي ألقيت في مختلف النقاط.

إذا أخذنا الكود التالي وقمت بتجميعه

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

بالنسبة لأولئك الذين لا يتكلمون bytecode ، فإن هذا يعادل تقريبا جافا جاوة التالية. اضطررت لاستخدام gotos لأن الـ bytecode لا يتطابق فعليًا مع تدفق التحكم في Java.

كما ترون ، هناك الكثير من الحالات للتعامل مع مختلف الاحتمالات من الاستثناءات المكبوتة. ليس من المعقول أن تكون قادرًا على تغطية كل هذه الحالات. في الواقع ، من المستحيل الوصول إلى فرع goto L59 في مربع المحاولة الأول ، حيث أن الصيد الأول "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)
}

لقد حصلت على بعض التعليمات البرمجية التي تستخدم في محاولة مع الموارد وفي jacoco انها تظهر فقط نصف تغطية. جميع خطوط شفرة المصدر خضراء ، لكنني أحصل على رمز أصفر صغير يخبرني بأن 4 فروع فقط من 8 فروع مغطاة.

أواجه مشكلة في معرفة ما هي جميع الفروع ، وكيفية كتابة التعليمات البرمجية التي تغطيها. ثلاثة أماكن محتملة رمي PipelineException . هذه هي createStageList() ، processItem() close() ضمني close()

  1. لا رمي أي استثناءات ،
  2. رمي استثناء من createStageList()
  3. رمي استثناء من processItem()
  4. رمي استثناء من close()
  5. رمي استثناء من processItem() close()

لا أستطيع التفكير في أي حالات أخرى ، ولكن لا يزال لدي فقط 4 من 8 حالات مغطاة.

هل يمكن لأي شخص أن يشرح لي لماذا هو 4 من 8 وهل هناك على أي حال لضرب جميع الفروع الثمانية؟ أنا لست ماهرا مع decterpting / القراءة / تفسير رمز بايت ، ولكن ربما كنت ... :) رأيت بالفعل https://github.com/jacoco/jacoco/issues/82 ، ولكن لا هذا ولا القضية تشير إلى المساعدة إلى حد كبير (بخلاف الإشارة إلى أن هذا بسبب كتل مجمعة مولدة)

حسنًا ، تمامًا كما أنتهيت من كتابة هذا ، كان لدي تفكير في حالة ما (ق) قد لا يتم اختبارها بما أشرت إليه أعلاه ... سأقوم بنشر إجابة إذا كنت على صواب. أنا متأكد من أن هذا السؤال والجواب سيساعد شخصًا ما في أي حال.

تحرير: كلا ، لم أجد ذلك. رمي RuntimeExceptions (لا تتم معالجتها بواسطة كتلة catch) لا يغطي أي فروع أخرى


أربع سنوات ، ولكن لا يزال ...

  1. المسار السعيد باستخدام AutoCloseable غير الفارغة
  2. مسار سعيد مع لاغ AutoCloseable
  3. يلقي على الكتابة
  4. يلقي على مقربة
  5. يلقي على الكتابة والإغلاق
  6. يلقي في مواصفات الموارد ( مع جزء ، على سبيل المثال استدعاء منشئ)
  7. يلقي في try كتلة ولكن AutoCloseable فارغ

أعلاه يسرد جميع الشروط 7 - سبب الفروع 8 يرجع إلى حالة متكررة.

يمكن الوصول إلى جميع الفروع ، try-with-resources هي عبارة عن سكر مجمّع بسيط إلى حد ما (على الأقل مقارنة switch-on-string ) - إذا لم يكن من الممكن الوصول إليه ، فعندئذ يكون بحساب البرمجي خطأ.

مطلوب فقط 6 اختبارات وحدة (في المثال رمز أدناه ، throwsOnClose هو @Ingore d وتغطية الفرع 8/8.

لاحظ أيضًا أن Throwable.addSuppressed(Throwable) لا يمكن أن Throwable.addSuppressed(Throwable) نفسه ، لذلك يحتوي bytecode الذي تم إنشاؤه على حارس إضافي (IF_ACMPEQ - مرجع المساواة) لمنع ذلك). لحسن الحظ يتم تغطية هذا الفرع من قبل رمي - على - كتابة ، رمي - على - - إغلاق - وإلقاء - على - كتابة - و - أغلق الحالات ، كما يتم إعادة استخدام فتحات المتغير كود بايت بواسطة المناطق الخارجية 2 من 3 معالج الاستثناء.

هذه ليست مشكلة مع Jacoco - في الواقع رمز المثال في https://github.com/jacoco/jacoco/issues/82 المرتبطة https://github.com/jacoco/jacoco/issues/82 غير صحيح حيث لا توجد عمليات فحص فارغة مكررة وليس هناك كتلة التقاط متداخلة حول الإغلاق.

اختبار 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)));
        }

    }
}

مذكرة قانونية

على الرغم من عدم وجود رمز عينة OP ، هناك حالة واحدة لا يمكن اختبارها AFAIK.

إذا قمت بتمرير مرجع المورد كوسيطة ، ففي Java 7/8 يجب أن يكون لديك متغير محلي لتعيينه إلى:

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

في هذه الحالة ، ستظل الشفرة التي تم إنشاؤها هي حراسة مرجع المورد. يتم تحديث السكر التركيبي في Java 9 ، حيث لم يعد المتغير المحلي مطلوبًا: try(arg){ /*...*/ }

إضافي - اقتراح استخدام المكتبة لتجنب الفروع تمامًا

من المسلم به أن بعض هذه الفروع يمكن شطبها على أنها غير واقعية - أي عندما تستخدم كتلة المحاولة AutoCloseable بدون فحص فارغ أو حيث لا يمكن أن يكون مرجع المورد ( with ) خاليًا.

في كثير من الأحيان لا يهتم التطبيق الخاص بك حيث فشلت - لفتح الملف ، والكتابة عليه أو إغلاقه - دقة التفاصيل لا علاقة لها بالموضوع (ما لم يكن التطبيق مهتمًا تحديدًا بالملفات ، مثل متصفح الملفات أو معالج النصوص).

علاوة على ذلك ، في كود OP ، لاختبار المسار القابل للغلق - يجب عليك إعادة تشكيل كتلة المحاولة إلى طريقة محمية ، فئة فرعية وتوفير تطبيق NOOP - كل هذا فقط الحصول على تغطية على الفروع التي لن يتم أخذها في البرية .

كتبت مكتبة Java 8 صغيرة io.earcam.unexceptional (في Maven Central ) التي تتعامل مع معظم الاستثناءات المعيارية المحددة.

ذو صلة بهذا السؤال: فهو يوفر مجموعة من الخطوط الفرعية ذات الفروع صفر لـ AutoCloseable s ، مما يؤدي إلى تحويل الاستثناءات المحددة إلى غير محدد.

مثال: Free Port Finder

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

كان لدي مشكلة مماثلة مع شيء من هذا القبيل:

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

شكت أن 2 من 8 فروع لم تكن مغطاة. انتهى به الأمر:

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

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

لا تغييرات أخرى وأنا الآن وصلت إلى 100 ٪ ....


قام جاكوكو مؤخراً بإصلاح هذه المشكلة ، الإصدار 0.8.0 (2018/01/02)

"أثناء إنشاء التقارير ، يتم تصفية العديد من المصنوعات الناتجة ، والتي تتطلب غير ذلك من الحيل غير الضرورية أو المستحيلة في بعض الأحيان لعدم وجود تغطية جزئية أو مفقودة:

  • جزء من كود bytecode لتجربة استخدام الموارد مع (GitHub # 500). "

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


يمكنني تغطية جميع الفروع الثمانية ، لذا فإن جوابي هو YES. انظر إلى الشفرة التالية ، هذه مجرد محاولة سريعة ، ولكنها تعمل (أو انظر جيثب: https://github.com/bachoreczm/basicjava وحزمة 'trywithresources' ، هناك يمكنك أن تجد ، كيف تجرّب- تعمل الموارد ، انظر فئة "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;
  }
}

Exception categories

When talking about exceptions I always refer back to Eric Lippert's Vexing exceptions blog article. He places exceptions into these categories:

  • Fatal - These exceptions are not your fault : you cannot prevent then, and you cannot sensibly handle them. For example, OutOfMemoryError or ThreadAbortException .
  • Boneheaded - These exceptions are your fault : you should have prevented them, and they represent bugs in your code. For example, ArrayIndexOutOfBoundsException , NullPointerException or any IllegalArgumentException .
  • Vexing - These exceptions are not exceptional , not your fault, you cannot prevent them, but you'll have to deal with them. They are often the result of an unfortunate design decision, such as throwing NumberFormatException from Integer.parseInt instead of providing an Integer.tryParseInt method that returns a boolean false on parse failure.
  • Exogenous - These exceptions are usually exceptional , not your fault, you cannot (reasonably) prevent them, but you must handle them . For example, FileNotFoundException .

An API user:

  • must not handle fatal or boneheaded exceptions.
  • should handle vexing exceptions, but they should not occur in an ideal API.
  • must handle exogenous exceptions.

Checked exceptions

The fact that the API user must handle a particular exception is part of the method's contract between the caller and the callee. The contract specifies, among other things: the number and types of arguments the callee expects, the type of return value the caller can expect, and the exceptions the caller is expected to handle .

Since vexing exceptions should not exist in an API, only these exogenous exceptions must be checked exceptions to be part of the method's contract. Relatively few exceptions are exogenous , so any API should have relatively few checked exceptions.

A checked exception is an exception that must be handled . Handling an exception can be as simple as swallowing it. There! The exception is handled. فترة. If the developer wants to handle it that way, fine. But he can't ignore the exception, and has been warned.

API problems

But any API that has checked vexing and fatal exceptions (eg the JCL) will put unnecessary strain on the API users. Such exceptions have to be handled, but either the exception is so common that it should not have been an exception in the first place, or nothing can be done when handling it. And this causes Java developers to hate checked exceptions.

Also, many APIs don't have a proper exception class hierarchy, causing all kinds of non-exogenous exception causes to be represented by a single checked exception class (eg IOException ). And this also causes Java developers to hate checked exceptions.

استنتاج

Exogenous exceptions are those that are not your fault, could not have been prevented, and which should be handled. These form a small subset of all the exceptions that can get thrown. APIs should only have checked exogenous exceptions , and all other exceptions unchecked. This will make better APIs, put less strain on the API user, and therefore reduce the need to catch all, swallow or rethrow unchecked exceptions.

So don't hate Java and its checked exceptions. Instead, hate the APIs that overuse checked exceptions.





java code-coverage bytecode jacoco try-with-resources