Почему я не люблю синтетические тесты
Мне не нравится, когда кто-то пытается использовать созданные вручную примеры кода для оценки возможностей статического анализатора кода. Сейчас на конкретном примере я продемонстрирую, почему негативно отношусь к синтетическим тестам.
Не так давно 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.
Дополнительные ссылки:
- Диагностические возможности PVS-Studio.
- База реальных ошибок, найденных с помощью PVS-Studio в открытых проектах.
- Мифы о статическом анализе. Миф пятый — можно составить маленькую программу, чтобы оценить инструмент.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Why I Dislike Synthetic Tests