Чому я не люблю синтетичні тести

Why I Dislike Synthetic Tests
Мені не подобається, коли хтось намагається використовувати створені вручну приклади коду для оцінки можливостей статичного аналізатора коду. Зараз на конкретному прикладі я покажу, чому негативно ставлюся до синтетичних тестів.

Не так давно Bill Torpey написав у своєму блозі замітку "Even Mo' Static", де розповів, як, на його погляд, показали себе інструменти Cppcheck і PVS-Studio при аналізі проекту itc-benchmarks. Проект itc-benchmarks — це static analysis benchmarks from Toyota ITC.

Мені не сподобалося, що після прочитання статті створюється враження, що аналізатори Cppcheck і PVS-Studio приблизно рівні у своїх можливостях. Зі статті випливає, що один аналізатор показує себе краще в одному, другий-в іншому, але в цілому їх діагностичні можливості схожі.

Я думаю, що це не так. Моя думка — наш аналізатор PVS-Studio в кілька разів могутніше, ніж Cppcheck. І взагалі, це не «думка», я знаю це!


Проте, саме з боку не видно, що PVS-Studio в 10 разів краще Cppcheck, то треба спробувати зрозуміти причину. Я вирішив подивитися на цей самий itc-benchmarks і розібратися, чому PVS-Studio показав себе на цій тестовій базі не кращим чином.

Чим далі я розбирався, тим більше роздратування я відчував. А один приклад зовсім вивів мене з рівноваги, і про нього я розповім трохи нижче. Мої висновки такі: у мене немає претензій до Bill Torpey. Він написав хорошу, чесну статтю. Спасибі, Bill. Зате у мене є претензії до Toyota ITC. Моя особиста думка: їх тестова база — хрень. Це, звичайно, гучна заява, але я вважаю, що маю достатню кваліфікацію та досвід, щоб міркувати про статичних аналізаторах коду і способи їх оцінки. На мою думку, itc-benchmarks не може бути використаний для адекватної оцінки можливостей того чи іншого аналізатора.

А ось власне тест, який остаточно вивів мене з душевної рівноваги.

тест на розіменування нульового покажчика:
void null_pointer_001 ()
{
int *p = NULL;
*p = 1; /*Tool should detect this line as error*/
/*ERROR:NULL pointer dereference*/
}

Аналізатор Cppcheck заявляє, що знайшов помилку в цьому коді:
Null pointer dereference: p

Аналізатор PVS-Studio на цьому коді мовчить, хоча для таких випадків в ньому існує діагностика V522.

Так що ж виходить, PVS-Studio на цьому прикладі слабкіше ніж Cppcheck? Ні, він як раз сильніше!

Аналізатор PVS-Studio розуміє, що цей код написаний свідомо і ніякої помилки тут немає.

Бувають ситуації, коли аналогічний код пишуть спеціально, щоб домогтися виникнення виключення при разіменуванні нульового покажчика. Таке можна зустріти в тестах або у специфічних ділянках коду. Подібний код ми неодноразово зустрічали. Ось, наприклад, як таке може виглядати в реальному проекті:
void GpuChildThread::OnCrash() {
LOG(INFO) << "GPU: Simulating GPU crash";
// Good bye, cruel world.
volatile int* it_s_the_end_of_the_world_as_we_know_it = NULL;
*it_s_the_end_of_the_world_as_we_know_it = 0xdead;
}

Тому в аналізаторі PVS-Studio в діагностиці V522 реалізовано декілька винятків, щоб не лаятися на такий код. Аналізатор бачить, що null_pointer_001 є несправжньою функцією. Не буває в реальному коді помилок у функціях, коли нуль записують в вказівник і відразу його разыменовывают. Та й ім'я функції каже аналізатору, що «нульовий покажчик» тут неспроста.

Для подібних випадків, у діагностиці V522 реалізовано виняток A6. Під нього ж потрапляє і синтетична функція null_pointer_001. Ось як небезпечно виняток A6:

Розіменування змінної знаходиться в функції, в назві якої є одне з слів:
  • error
  • default
  • crash
  • null
  • test
  • violation
  • throw
  • exception
При цьому, змінної присвоюється 0 рядком вище.

Синтетичний тест повністю підійшов під це виняток. По-перше, в назві функції є слово «null». По-друге, призначення нуля змінної відбувається якраз на попередньому рядку. Виняток виявило несправжній код. Код дійсно не справжній, це синтетичний тест.

Ось з-за подібних нюансів я і не люблю синтетичні тести!

У мене є до itc-benchmarks й інші претензії. Наприклад, все в тому ж файлі, ми можемо бачити ось такий тест:
void null_pointer_006 ()
{
int *p;
p = (int *)(intptr_t)rand();
*p = 1; /*Tool should detect this line as error*/
/*ERROR:NULL pointer dereference*/
}

Функція rand може повернути 0, який потім перетвориться в NULL. Аналізатор PVS-Studio поки не знає, що може повернути rand і тому не бачить в цьому коді нічого підозрілого.

Я попросив колег навчити аналізатор краще розуміти, що з себе представляє функція rand. Діватися нікуди, доведеться підточити напилком аналізатор, щоб він краще себе показував на розглянутій тестовій базі. Це вимушена міра, раз для оцінки аналізаторів використовують подібні набори тестів.

Але не бійтеся. Я заявляю, що ми як і раніше будемо працювати над цими гарними діагностикою, а не займатися підгонкою аналізатора під тести. Мабуть, ми трохи підретушуємо PVS-Studio для itc-benchmarks, але у фоновому режимі і тільки в тих місцях, які мають хоч якийсь сенс.

Я хочу, щоб розробники розуміли, що приклад rand нічого насправді не оцінює. Це синтетичний тест, який висмоктаний з пальця. Не пишуть так програми. Немає таких помилок.

До речі, якщо функція rand поверне 0, а 1400 краще не буде. Все одно такий покажчик розв'язати не можна. Так що розіменування нульового покажчика якийсь дивний приватний випадок абсолютно некоректного коду, який просто був вигаданий і яка не зустрічається в реальних програмах.

Мені відомі справжні программистские біди. Наприклад, це помилки, які ми виявляємо сотнями, скажімо, з допомогою діагностики V501. Що цікаво, в itc-benchmarks я не помітив жодного тесту, де перевірявся, може аналізатор виявити помилку виду «if (a.x == a.x)». Жодного тесту!

Таким чином, itc-benchmarks ігнорує можливості аналізаторів з пошуку помилок. А читачі наших статей знають, наскільки це поширені помилки. Зате містить, на мій погляд, безглузді тестові кейси, які в реальних програмах не зустрічаються. Я не можу уявити, що в цьому серйозному проекті можна зустріти ось такий код, який приводить до виходу за кордон масиву:
void overrun_st_014 ()
{
int buf[5];
int index;
index = rand();
buf[index] = 1; /*Tool should detect this line as error*/
/*ERROR: buffer overrun */
sink = buf[idx];
}

Мабуть, таке можна зустріти хіба тільки в лабораторних роботах студентів.

При цьому я знаю, що в серйозному проекті легко зустріти помилку виду:
return (!strcmp (a->v.val_vms_delta.lbl1,
b->v.val_vms_delta.lbl1)
&& !strcmp (a->v.val_vms_delta.lbl1,
b->v.val_vms_delta.lbl1));

Цю помилку аналізатор PVS-Studio знайшов код компілятора GCC. Два рази порівнюються одні і ті ж рядки.

Виходить, що тести на пошук екзотичного коду rand є, а на класичні помилки немає.

Я можу продовжувати і далі, але, мабуть, зупинюся. Я виговорився і мені стало легше. Дякую за увагу. Тепер у мене є стаття-відповідь, з моєю думкою про синтетичних базах помилок.

Пропоную всім встановити і спробувати потужний аналізатор коду PVS-Studio.

посилання:
  1. Діагностичні можливості PVS-Studio.
  2. База реальних помилок, знайдених з допомогою PVS-Studio у відкритих проектах.
  3. Міфи про статичному аналізі. Міф п'ятий – можна скласти маленьку програму, щоб оцінити інструмент.




Якщо хочете поділитися цією статтею з англомовної аудиторією, то прошу використовувати посилання на переклад: Andrey Karpov. Why I Dislike Synthetic Tests

Прочитали статтю і є питання?Часто до наших статей задають одні і ті ж питання. Відповіді на них ми зібрали тут: Відповіді на питання читачів статей про PVS-Studio, версія 2015. Будь ласка, ознайомтеся зі списком.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.