Мутационное тестирование ядра ОС
Опыт с Mull был не совсем успешным (не только из-за незрелости самого инструмента!) и желание попробовать MT всё еще оставалось. После всех экспериментов я переключился на другой проект и там Mull не получилось бы использовать: в качестве основного компилятора используется GCC и тесты написаны не на Google Test, а на своём собственном фреймворке. Поэтому всё пришлось делать с нуля, в том числе и выбор инструмента.
Читая о проекте формальной верификации в Astra Linux я узнал про инструмент Frama-C. Это статический анализатор для кода на C. Отличие его от других статических анализаторов в том, что можно писать для него расширения и он поддерживает язык разметки ACSL, который используют для описания предикатов. Я рассуждал так: если это статический анализатор с возможностью расширения, то теоретически для него можно написать расширение, которое может добавлять мутации в код. Оказалось, что так рассуждал не только я и такое расширение уже есть.
Что надо автоматизировать для MT:
- запустить тесты по дефолту и собрать результаты
- сделать препроцессинг исходного файла (см. раздел 5.1 в документации)
- сделать мутацию для исходного файла
- пересобрать проект
- запустить тесты и собрать результаты
- сравнить оригинальные результаты и предыд результаты
- сохранить мутацию и результат прогона
- перейти в п.2
Как выглядит мутация исходного кода с Frama-C:
int max(int i, int j) { return (i < j) ? j : i; }
int main() {
max(1, 0);
return 0;
}
$ frama-c sample.c -mut -mut-code -mut-spec -mut-summary-file SUMMARY
[kernel] Parsing FRAMAC_SHARE/libc/__fc_builtin_for_normalization.i (no preprocessing)
[kernel] Parsing sample.c (with preprocessing)
$
$ ls -1 mutant_*
mutant_0.c
mutant_1.c
mutant_2.c
mutant_3.c
mutant_4.c
mutant_5.c
$
В файл SUMMARY находятся список названий файлов с мутациями и сама мутация:
$ cat summary.csv
mutant_0.c,sample.c:2: `<` --> `!=`
mutant_1.c,sample.c:2: `<` --> `==`
mutant_2.c,sample.c:2: `<` --> `>=`
mutant_3.c,sample.c:2: `<` --> `>`
mutant_4.c,sample.c:2: `<` --> `<=`
mutant_5.c,sample.c:2: `i < j` --> `! (i < j)`
$ frama-c FILE -mut [-mut-code] [-mut-spec] [-mut-summary-file SUMMARY]
* Replacement of a binary operator
* Condition reversal
* Loop invariant deletion
* Postcondition deletion
* Conjunction pruning
* Replacement of numerical values
Кратко о том, как это выглядело: выполняется обычная сборка проекта (make os
)
и проверка того, что тесты успешно проходят (make -C os qemutests/run
). Во
время сборки скрипт находит строку для компиляции нашего файла в выводе gcc,
это нужно, чтобы Frama-C нашла все необходимые заголовочные файлы. Потом скрипт
c помощью Frama-C и плагина для мутаций генерирует для целевого файла N-файлов
с одной мутацией в каждом. Далее подменяем в дереве исходников целевой файл на
файл с мутацией и запускаем пересборку с последующим запуском тестов. После
обработки каждого исходного файла проекта я отдельным скриптом генерировал
простенький HTML-отчет.
Всего было сделано ~3200 мутаций для исходных файлов в разных подсистемах ядра
(mm
, task
, thread
, vmm
, io
) и результаты выполнения тестов на мутированном
коде можно поделить на несколько категорий:
- ОС не смогла успешно загрузиться в эмуляторе QEMU и генерировала исключение
- ОС зависала во время загрузки в QEMU
- ОС загружалась и тесты проходили успешно
Случаи 1 и 2 нас не сильно интересуют. Логи для результаты тестов из 3-й категории я просматривал вручную и нашел строки, которые у нас покрыты тестами, но при мутации в этой строке тесты проходят успешно. То есть это именно то, ради чего все и затевалось. Нашлось как минимум 35 мутаций с которыми тесты проходили успешно, при том, что строки, в которые вносились мутации были покрыты этими тестами.
Всё тестирование для перечисленных ниже файлов заняло примерно 1.5 недели, в основном из-за того, что если ОС зависает, то выполнение тестов прерывается по таймауту, который составляет примерно 3 мин. Если тесты проходят успешно, итерация с одной мутацией занимает ~10 сек.
Из проблем с Frama-C:
- Frama-C требует название входной функции (аргумент
-main
), и не понятно что делать в случае мутаций множества файлов. Если добавить пустую функцию main(), то Frama-C не ругается. - Портился вывод в CSV формат и пришлось сделать патч для расширения Frama-C-Mutation
- Исходный код проекта потребовал небольших изменений, потому что Frama-C
отказывалась выдавала ошибки. Предполагаю, что Frama-C не настолько крутой
парсер конструкций C, как например в промышленных компиляторах типа
clang
илиgcc
.
Дальше решили проверить библиотеку для ядра ОС и тут возникли проблемы. Frama-C смогла распарсить не весь код на Си. Для некоторых ошибок можно было быстро поправить или закомментировать строчки, но с некоторыми ошибками разобраться не получилось.
В статье про тестирование RCU написано, что они использовали инструмент из Is Mutation an Appropriate Tool for Testing Experiments?, потому что он простой (несколько сот строк на Prolog и Shell). (“8.4% of resulting mutants did not compile”). Я написал автору статьи Alex Groce и он посоветовал использовать: cmutate или universalmutator, описанный в другой статье.
Подход universalmutator мне показался слишком простым: инструмент использует использует регулярные выражения для внесения мутаций и после каждой мутации сохраняет исходный файл с изменением. Остальную часть работы нужно делать самому. Строить синтаксическое дерево ей не требуется, и из-за этого мутации вносятся без учёта семантики языка. Большого выбора не было и я решил её попробовать. При запуске в ручном режиме на первом же файле после запуска тестов выжили четыре мутанта. Поэтому я написал скрипты для автоматизации работы и запустил уже для всех исходных файлов.
Резюме: очевидно, что польза от MT есть. Для внедрения в регулярное тестирование нужно сделать такое тестирование полностью автоматическим.