לדלג לתוכן

פתרון הוכחת איכות ספרייה

אפשר להוסיף לתוכנית מההרצאה את הבדיקה הבאה. היא משתמשת במחולל מוגדר וב־seed מפורש, ומשווה כל מספר למחרוזת שיצרה הספרייה התקנית.

#include <random>
#include <string>

void property_tests() {
    std::mt19937 random(0xA17u);
    for (int i = 0; i < 1000; ++i) {
        auto value = static_cast<std::uint32_t>(random());
        round_trip(value);
        auto text = std::to_string(value);
        for (int zeros = 1; zeros <= 16; ++zeros) {
            text.insert(text.begin(), '0');
            auto parsed = parse_u32(text);
            assert(std::holds_alternative<std::uint32_t>(parsed));
            assert(std::get<std::uint32_t>(parsed) == value);
        }
    }
}

הקוד מוסיף בכל סיבוב אפס נוסף, ולכן בודק אורכים שונים מאחת עד שש־עשרה ספרות מובילות. הוא אינו מוכיח שהאלגוריתם עומד בדרישות זמן עבור קלט של מיליארדי תווים, וגם אינו בודק תצוגה שמצביעה לזיכרון שכבר שוחרר.

הקלט 4294967296 הוא דוגמה ברורה לתקלה בגרסה שמחשבת לפני הבדיקה. הערך גדול באחד מהמקסימום של uint32_t, ולכן חישוב unsigned במודולו המתאים יכול להחזיר אפס. בדיקה מאוחרת מסוג value > maximum לעולם לא תצליח, כי הטיפוס עצמו אינו יכול להכיל ערך גדול מהמקסימום.

הגרסה התקינה הופכת את השאלה. במקום לחשב ואז לבדוק, היא שואלת אם הערך הקיים קטן מספיק כדי שהכפל והחיבור הבאים יהיו מותרים. מכיוון שהספרה כבר נבדקה, אפשר לחסר אותה מהמקסימום בבטחה.

בדיקת compile-fail מתאימה לממשק מהשיעור 2.2 יכולה להשתמש בטיפוס שאין לו פעולת חיבור. חשוב לכלול גם קובץ חיובי עם אותו header, כדי שכשל בגלל נתיב include לא ייראה כאילו ה־concept עבד.

struct NoAddition {};
// The following line belongs in a separate negative test.
auto invalid = add_closed(NoAddition{}, NoAddition{});

השם add_closed צריך להיות זהה לפונקציה שהוגדרה במימוש התלמיד. הקובץ השלילי אינו חלק מתוכנית הבדיקות שנבנית בהצלחה; מערכת ההרצה מפעילה עליו מהדר בנפרד ובודקת שהבנייה נכשלת.

ה־fuzzer יכול לחשוף קריאה מחוץ לגבול, לולאה אינסופית או תקלה מסוימת שמזהה sanitizer. הוא אינו יודע אוטומטית אם מספר שהוחזר הוא המספר הנכון, אם הובטח זמן ריצה מסוים, או אם הודעת השגיאה מתאימה לחוזה. הוספת oracle ותכונות הופכת חלק מהדרישות האלה לבדיקה.

במכולה בעלת state נדרש גם תרחיש של רצף פעולות. צריך לשלב הוספה, העברה, מחיקה, כשל הקצאה והשמדה, ולבדוק אינווריאנטים אחרי כל שלב. מונה הקצאות לבדו אינו מספיק, כי אפשר לשחרר את כל הזיכרון ועדיין להחזיר ערך שגוי או להשתמש באובייקט אחרי סוף חייו.

בדוח טוב מפרידים בין בדיקה שרצה לבין פקודה מוצעת. אם אין סביבת libFuzzer, מציינים שהיעד נכתב אך לא הורץ, ומציגים בינתיים את בדיקות הדוגמאות והתכונות שניתן להריץ בכל מהדר C++20 מתאים.