سلسلة ++c للمحترفين اختبار الوحدات وأدوات تنقيح الشيفرات وتصحيح الأخطاء في Cpp


محمد الميداوي

يقضي مطوّرو C++‎ الكثير من وقتهم في تنقيح الأخطاء (debugging)، ويهدف هذا المقال إلى مساعدتك في هذه المهمة وإعطائك بعض التقنيات المفيدة، لكن لا تتوقع قائمة شاملة للمشاكل وحلولها التي تدعمها هذه الأدوات.

اختبار الوحدات في C++‎

يسعى اختبار الوحدات (Unit testing) إلى التحقق من صحّة وسلامة وحدات الشيفرة (units of code)، ويشير مصطلح "وحدات الشيفرة" في C++‎ غالبًا إلى الأصناف أو الدوال أو مجموعات مؤلّفة منهما.

ويُجرى اختبار الوحدات غالبًا باستخدام "أُطر اختبار" متخصصة أو "مكتبات اختبار"، والتي تستخدِم غالبًا صياغات أو أنماط غير اعتيادية، وسنراجع في هذا الموضوع بعض استراتيجيات ومكتبات وأطر اختبار الوحدات.

اختبار جوجل

اختبار جوجل (Google Test) هو إطار لاختبار C++‎ من Google، ويتطلّب استخدامُه بناء مكتبة ‎gtest‎ وربطها بإطار الاختبار الخاص بك.

مثال بسيط، سنقسم المثال بداعي الشرح …

// main.cpp
#include <gtest/gtest.h>
#include <iostream>

أنشئت حالات اختبار جوجل بواسطة وحدات ماكرو خاصة بالمعالج الأولي لـ ++C، وسنوفر المعامِليْن test name و test suite، نتابع …

TEST(module_name, test_name) {
    std::cout << "Hello world!" << std::endl;

كذلك توفر Google Test وحدات ماكرو لأجل التوكيدات (assertions):

    ASSERT_EQ(1+1, 2);
}

يمكن تشغيل اختبار جوجل يدويًا من دالة ()main أو ربطها بمكتبة gtest_main لدالة ()main معدَّة سلفًا لقبول حالات اختبار جوجل، …

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}
// أمر البناء: g++ main.cpp -lgtest

catch

Catch هي مكتبة ترويسة (header only library) تسمح لك باستخدام كل من نمطي الاختبار TDD و BDD.

الشيفرة التالية مأخوذة من صفحة توثيق Catch:

SCENARIO("vectors can be sized and resized", "[vector]") {
    GIVEN("A vector with some items") {
        std::vector v(5);

        REQUIRE(v.size() == 5);
        REQUIRE(v.capacity() >= 5);

        WHEN("the size is increased") {
            v.resize(10);

            THEN("the size and capacity change") {
                REQUIRE(v.size() == 10);
                REQUIRE(v.capacity() >= 10);
            }
        }
        WHEN("the size is reduced") {
            v.resize(0);

            THEN("the size changes but not capacity") {
                REQUIRE(v.size() == 0);
                REQUIRE(v.capacity() >= 5);
            }
        }
        WHEN("more capacity is reserved") {
            v.reserve(10);

            THEN("the capacity changes but not the size") {
                REQUIRE(v.size() == 5);
                REQUIRE(v.capacity() >= 10);
            }
        }
        WHEN("less capacity is reserved") {
            v.reserve(0);

            THEN("neither size nor capacity are changed") {
                REQUIRE(v.size() == 5);
                REQUIRE(v.capacity() >= 5);
            }
        }
    }
}

سيُبلَغ عن هذه الاختبارات على النحو التالي عند التشغيل:

Scenario: vectors can be sized and resized Given: A vector with some items When: more capacity is reserved Then: the capacity changes but not the size

التحليل الساكن Static analysis

التحليل الساكن طريقة لتفحّص الشيفرة للبحث عن الأنماط التي ترتبط عادة بالأخطاء المشهورة وتدل عليها، هذه التقنية تستغرق وقتًا أقل من مراجعة الشيفرة، لكن تقتصر عمليات التحقق التي تقوم بها على الحالات المُبرمجة في الأداة. وتتضمّن عمليات مثل التحقّق استخدام الفاصلة المنقوطة بشكل غير صحيح بعد عبارة ‎(if (var);)‎ مثلًا، وخوارزميات الشُّعَب (graph algorithms) المتقدمة التي تحدد ما إذا كان المتغيّر مُهيَّأً أم لا.

تحذيرات المُصرِّف Compiler warnings

من السهل تمكين التحليل الساكن، فهناك إصدارات مُبسّطة مُضمّنة سلفًا في مصرّفك:

إذا مكّنتَ هذه الخيارات، ستلاحظ أنّ كل مُصرِّف سيعثر على أخطاء لا تنتبه إليها المصرّفات الأخرى، وستحصل على أخطاء على تقنيات قد تكون صالحة في سياقات محدّدة. فمثلًا، قد تكون ‎while (staticAtomicBool);‎ مقبولة حتى لو لم تكن ‎while (localBool);‎ كذلك.

لذلك، على عكس مراجعة الشيفرة، فأنت تستخدم أداة تفهم شيفرتك، وتنبّهك إلى الكثير من الأخطاء، وأحيانًا قد لا تتفق معك. قد تضطر إلى وقف التحذير محليًا في هذه الحالة نظرًا لأنّ الخيارات المذكورة أعلاه قد تُمكِّن جميع التحذيرات، بما في ذلك التحذيرات التي لا تريدها (مثلًا، لماذا يجب أن تكون شيفرتك متوافقة مع C++‎ 98؟). في هذه الحالة، يمكنك تعطيل تحذير بعينه:

  • clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...‎

  • ‎‎‎‎g++ -Wall -Weverything -Werror -Wno-errortoaccept ...‎

  • ‎‎‎‎cl.exe /W4 /WX /wd<no of warning>...‎

    صحيح أنّ تحذيرات المصرّف ستساعدك أثناء التطوير، إلّا أنّها تُبطئ عملية التصريف، لهذا قد لا ترغب في تمكينها افتراضيًا في كل مرة، فإما أن تشغّل تحذيرات المصرّف افتراضيًا، أو يمكنك تمكين التكامل المستمر (continuous integration) باستخدام عمليات تحقق أكثر كلفة (أو كليهما).

أدوات خارجية

إذا قرّرت العمل بالتكامل المستمر (continuous integration)، فإنّ استخدام الأدوات الأخرى ليست ذات فائدة. وتحتوي أداة مثل clang-tidy على قائمة من عمليّات التحقق التي تغطي مجموعة واسعة من المشاكل، منها:

  • الأخطاء الفعلية
  • منع التشريح (Prevention of slicing)
  • توكيدات (Asserts) مع آثار جانبية
  • التحقّق من إمكانية القراءة
  • إزاحات بادئة (indentation) غير صحيحة
  • التحقق من تسمية المُعرِّف (Check identifier naming)
  • التحقّق من التحديث (Modernization checks)
  • استخدام make_unique()‎
  • استخدام nullptr *التحقّق من الأداء
  • إيجاد النسخ غير الضرورية
  • البحث عن خوارزميات الاستدعاء غير الفعّالة

قد لا تكون القائمة كبيرة، إذ أنّ Clang تحتوي على الكثير من التحذيرات، إلّا أنها ستجعل شيفرتك أكثر أمانًا.

أدوات أخرى

توجد أدوات أخرى مماثلة بنفس الغرض، مثل:

الخلاصة

توجد الكثير من أدوات التحليل الساكن لـ C++‎، سواء المضمّنة في المُصرِّف، أو الأدوات الخارجية، ولا تأخذ تجربتها الكثير من الوقت عند الاكتفاء بالعمل بالإعدادات السهلة، وستساعدك على رصد أخطاء قد لا تنتبه لها عند مراجعة الشيفرة.

تحليل Segfault مع GDB

سنستخدم نفس الشيفرة التي استخدمناها في المثال أعلاه.

#include <iostream>

void fail() {
    int *p1;
    int *p2(NULL);
    int *p3 = p1;
    if (p3) {
        std::cout << *p2 << std::endl;
    }
}
int main() {
    fail();
}

أولًا، سنحاول تصريفها:

g++ -g -o main main.cpp

ثم نشغّلها بواسطة gdb:

gdb ./main

الآن، سنكون في صدفة gdb. اكتب الأمر run.

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/opencog/code-snippets/stackoverflow/a.out
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400850 in fail () at debugging_with_gdb.cc:11
11 std::cout << *p2 << std::endl;

لاحظ أنّ خطأ التجزئة (segmentation fault) قد حدث في السطر 11، لذا فالمتغيّر الوحيد المُستخدم في هذا السطر هو المؤشّر p2. يمكن التحقّق من محتواه بطباعة ما يلي:

(gdb) print p2
$1 = (int *) 0x0

نلاحظ أنّ p2 هُيِّئ عند القيمة 0×0، والتي تكافئ NULL. في هذا السطر، حاولنا تحصيل (dereference) مؤشّر فارغ، لذا علينا إصلاح هذا الخلل.

تنظيف الشيفرات

يبدأ التنقيح بفهم الشيفرة التي تحاول تنقيحها. انظر المثال التالي عن شيفرة سيّئة:

int main() {
    int value;
    std::vector < int > vectorToSort;
    vectorToSort.push_back(42);
    vectorToSort.push_back(13);
    for (int i = 52; i; i = i - 1) {
        vectorToSort.push_back(i *2);
    }
    /// مُحسّنة لاستخدامها في ترتيب المتجهات الصغيرة
    if (vectorToSort.size() == 1);
    else {
        if (vectorToSort.size() <= 2)
            std::sort(vectorToSort.begin(), std::end(vectorToSort));
    }
    for (value: vectorToSort) std::cout << value << ' ';
    return 0;
}

هذه الشيفرة أفضل:

std::vector < int > createSemiRandomData() {
    std::vector < int > data;
    data.push_back(42);
    data.push_back(13);
    for (int i = 52; i; --i)
        vectorToSort.push_back(i *2);
    return data;
}
    /// مُحسّنة لاستخدامها في ترتيب المتجهات الصغيرة
void sortVector(std::vector &v) {
    if (vectorToSort.size() == 1)
        return;
    if (vectorToSort.size() > 2)
        return;
    std::sort(vectorToSort.begin(), vectorToSort.end());
}
void printVector(const std::vector<int> &v) {
    for (auto i: v)
        std::cout << i << ' ';
}
int main() {
    auto vectorToSort = createSemiRandomData();
    sortVector(std::ref(vectorToSort));
    printVector(vectorToSort);
    return 0;
}

بغض النظر عن نمط التشفير الذي تفضله، سيساعدك الثّبات على نفس النمط على فهم شيفرتك، ويمكن رصد فرصتين لتحسين قراءة الشيفرة بالنظر إلى الشيفرة أعلاه، وكذلك تسهيل تنقيحها:

استخدام دوال منفصلة للإجراءات المنفصلة

يتيح لك استخدام دوال منفصلة تخطّي بعض الدوال في المنقّح إذا لم تكن مهتمًّا بتفاصيلها. قد لا تكون مهتمًا بتفاصيل إنشاء البيانات أو طباعتها في هذه الحالة الخاصّة، وقد ينصبّ اهتمامك على عملية الترتيب وحَسب.

هناك ميزة أخرى، وهي أنّ الشيفرة التي عليك قراءتها وحفظها ستكون أقصر عند تفحّص الشيفرة، إذ ستحتاج الآن إلى قراءة 3 أسطر فقط من الشيفرة في دالة ‎main()‎ لفهمها، بدلًا من قراءة الدالّة بأكملها.

والميزة الثالثة هي أنّه نظرًا لكون الشيفرةً المُتحقَّق منها أقصر، فإن عينك ستتدرب الآن على رصد الأخطاء في غضون ثوانٍ.

استخدام تنسيقات وإنشاءات متّسقة

استخدامُ التنسيقات والإنشاءات المتّسقة ينظّم الشيفرةَ ويسهّل تحليلها، وهناك نقاشات كثيرة حول طريقة التنسيق "الأمثل". لكن بغضّ النظر فالمهمّ أن تلتزم بنمط برمجة واحد في شيفرتك، فذلك سيحسّن قراءتها ويسهّل التركيز على محتوى الشيفرة.

ويوصى باستخدام أداة مخصّصة لتنسيق الشيفرة نظرًا لأنّ هذه المهمّة قد تستغرق وقتًا طويلاً، وتدعم معظم بيئات التطوير المتكاملة (IDEs) هذه الميزة بشكل أو بآخر.

قد تلاحظ أنّ نمط البرمجة لا يقتصر على استخدام المسافات الفارغة والسطور الجديدة، فلم نعد نمزج بين النمط الحر (free-style) والدوال التابعة لبدء / إنهاء الحاويات (‎v.begin()‎ و ‎std::end(v)‎).

تسليط الضوء على الأجزاء الهامّة من شيفرتك

بغض النظر عن النمط الذي تستخدمه، فإنّ الشيفرة أعلاه تحتوي على عدد من الملاحظات التي قد تعطيك تلميحًا حول الأمور المهمّة في الشيفرة:

  • تعليق يتضمّن الكلمة "مُحسَّن" ، في إشارة إلى استخدام بعض التقنيات الجذابة.
  • بعض تعليمات return المبكّرة في ‎sortVector()‎ تشير إلى أنّنا نريد فعل شيء خاصّ.
  • ‎std::ref()‎ - تشير إلى أنّ شيئًا ما يحدث في ‎sortVector()‎.

الخلاصة

الشيفرات النظيفة أسهل في الفهم وتقلل الوقت اللازم لتنقيحهَا، فقد يتمكّن مُراجِع الشيفرة من رصد الأخطاء منذ الوهلة الأولى في المثال الثاني، بينما قد تكون الأخطاء أخفَى في المثال الأوّل. (ملاحظة الخطأ موجود في: عملية الموازنة مع العدد ‎2‎.)

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 142: C++ Debugging and Debugprevention Tools & Techniques والفصل Chapter 141: Unit Testing in C++‎ من كتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن