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




code-coverage bytecode (4)

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

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

لقد حصلت على بعض التعليمات البرمجية التي تستخدم في محاولة مع الموارد وفي 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) لا يغطي أي فروع أخرى


حسناً ، لا أستطيع أن أخبركم ما هي المشكلة بالضبط مع 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)
}

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

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

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

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

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

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


لا سؤال حقيقي ، ولكن أراد أن يلقي المزيد من البحث هناك. tl؛ dr = يبدو أنك تستطيع تحقيق تغطية بنسبة 100٪ للتجربة أخيرًا ، ولكن ليس للتجربة باستخدام الموارد.

من المفهوم أن هناك اختلافًا بين محاولة المدرسة القديمة وأخيراً محاولة استخدام Java7 مع الموارد. في ما يلي مثالان مكافئان يعرضان الشيء نفسه باستخدام طرق بديلة.

مثال المدرسة القديمة (نهج محاولة أخيرة):

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

مثال Java7 (نهج محاولة مع الموارد):

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

التحليل: مثال المدرسة القديمة:
باستخدام Jacoco 0.7.4.201502262128 و JDK 1.8.0_45 ، تمكنت من الحصول على تغطية 100٪ من سطر ، وتعليمات وفروع على مثال المدرسة القديمة باستخدام الاختبارات الأربعة التالية:

  • مسار الشحوم الأساسي (العبارة غير فارغة ، ويتم تنفيذ () بشكل طبيعي)
  • تنفيذ () يطرح استثناء
  • foo () يتم عرض استثناء وعبارة كـ null
  • البيان عاد ك null
Jacoco يشير إلى فرعين داخل 'try' (على الاختيار فارغة) و 4 داخل النهاية (على الاختيار فارغة). كلها مغطاة بالكامل.

التحليل: مثال java-7:
إذا كانت نفس 4 اختبارات تعمل على مثال نمط Java7 ، فإن jacoco تشير إلى أن 6/8 الفروع يتم تغطيتها (في المحاولة نفسها) و 2/2 في خانة الاختيار الفارغ داخل المحاولة. لقد جربت عددًا من الاختبارات الإضافية لزيادة التغطية ، لكن لا يمكنني إيجاد طريقة للحصول على أفضل من 6/8. كما أشار البعض الآخر ، فإن الشفرة التي تم تفكيكها (والتي قمت بدراستها أيضًا) لمثال java-7 تشير إلى أن مترجم java يقوم بإنشاء مقاطع غير قابلة للوصول لمحاولات المورد. يقوم جاكوكو بالإبلاغ (بدقة) عن وجود مثل هذه الأجزاء.

تحديث: باستخدام نمط Java7 coding ، قد تتمكن من الحصول على تغطية 100٪ IF باستخدام Java7 JRE (راجع استجابة Matyas أدناه). ومع ذلك ، باستخدام نمط Java7 الترميز مع Java8 JRE ، أعتقد أنك ستضرب فروع 6/8 المغطاة. نفس الرمز ، مجرد JRE مختلفة. يبدو أن كود البايت يتم إنشاؤه بشكل مختلف بين جهازي JRE مع Java8 لإنشاء مسارات غير قابلة للوصول.





try-with-resources