Привет, Хабр.
Скорее всего то, о чем я сейчас расскажу, уже было реализовано и не единожды.
Но пусть это все равно лежит здесь, возможно эта статья будет кому‑то полезна в качестве методического материала или HOWTO. Все, сказанное ниже является продуктом моего текущего опыта разработки и не претендует на идеальное решение.
Итак, проблема
Каждый, уважающий себя проект, проводит юнит‑тестирование (оно же модульное тестирование), а также покрытие кода. В данной статье я рассчитываю, что читатель знаком со словами «юнит‑тестирование» и «покрытие кода» и не буду разбирать эти понятия в рамках данной статьи. Для последнего, я использую программу gcov из семейства GCC.
GCC — это GNU Compilers Collection, набор компиляторов проекта GNU. Прежде всего это компиляторы C/C++ (gcc и g++). GCC входит в тройку лидирующих C/C++ компиляторов: GCC, Clang, MSVC.
В качестве библиотеки для проведения юнит‑тестирования в данной статье я буду использовать check (который ‑lcheck ‑lsubunit), потому как и сам код проекта (совершенно открытого и академического, ссылка будет внизу), на базе которого я буду приводить примеры, написан на языке C.
Но с тем же успехом вы можете использовать вашу любимую библиотеку, который вы обычно используете, например Google C++ Testing Framework (который ‑lgtest ‑lgmock) для C++.
Те, кто применяет line‑based и branch‑based покрытие, знает как тяжело поднять процент покрытия на этих самых branch’ах (далее — ветках). Особенно, это касается файловых дескрипторов (об этом возможно, когда‑нибудь выйдет отдельная статья) и динамического выделения памяти. Бывает, уже перебрал в тестах все доступные тебе «плохие» варианты, но зеленой зоны покрытия (90%) так и не достиг. Хочу привести пример кода такой функции:

Line‑based покрытие в зеленой зоне (91.3%), а branch‑based — как всегда нет (53.8%). Давайте рассмотрим, что здесь происходит.
Функция динамически выделяет в памяти структуру и матрицу в ней. Код построен блоками так, что каждый последующий блок не будет выполняться, если работа предыдущего блока завершилась с ошибкой. Первый блок (в строке 22) запрашивает память размера структуры, второй (в строке 28) — массива указателей на строки матрицы и третий (в строке 36) — пытается аллоцировать сами строки матрицы. Беда в том, что у них это всегда получается.
-
Как видно из строки 15, ветки плохих аргументов мы проверили.
-
Ветки строки 42 не реализованы, потому что не наступает такой ситуации, когда результирующий указатель уже получил адрес, но при этом возникла бы ошибка в дальнейшей работе функции. Другими словами, чтобы первый блок отработал успешно, а ошибки возникли в следующих.
И даже если бы мы нашли способ (а такие способы есть) запретить выделение памяти при работе теста вообще — это не принесло бы нам сколько‑нибудь значительного результата. Было бы примерно так:

Лучше. Но недостаточно лучше. Покрытие ветвей выросло до 76.9%. Что именно произошло?
-
Тест не смог выделить память в первом же блоке и поднял покрытие ветки в строках 23–24. Также, случилось нужное нам ветвление в строках 26, 33 и 42, где следующие блоки поняли, что то‑то идет не так. В остальном все осталось так же. Блок самоочистки так и не наступил, потому как результирующий указатель все еще пуст. Что дальше? Запрашивать какой‑то гигантский размер памяти через аргумент size? Возможно у нас и получится удивить систему, запрашивая матрицу размером 65 535×65 535 (максимальное число, которое влезет в аргумент, который uint16_t). Но давайте поищем способ, дающий более точный результат, чем «возможно».
Решение
Нам нужен способ ограничивать выделение памяти, причем:
-
Не во время компиляции, а «на ходу».
-
Точечно, в тех блоках, которые мы хотим протестировать.
Как нам это сделать? А вот как:
Мы напишем свои, кастомные функции‑обертки над функциями malloc() и calloc() из стандартной библиотеки:
#include <dlfcn.h> void *malloc(size_t __size) { void *result = NULL; void *(*libc_malloc)(size_t) = NULL; *(void **)(&libc_malloc) = dlsym(RTLD_NEXT, "malloc"); if (!memory_locked(__size, 0)) result = libc_malloc(__size); return result; } void *calloc(size_t __nmemb, size_t __size) { void *result = NULL; void *(*libc_calloc)(size_t, size_t) = NULL; *(void **)(&libc_calloc) = dlsym(RTLD_NEXT, "calloc"); if (!memory_locked(__size, 0)) result = libc_calloc(__nmemb, __size); return result; }
Что здесь происходит? Мы взяли библиотеку для работы с динамически подключаемыми библиотеками (простите за тавтологию) dlfcn (которая -ldl) и используем её функцию dlsym() для получения адреса оригинальной функции в стандартной библиотеке libc.
После этого мы используем некоторый хедтрик, для того чтобы превратить полученный адрес в адрес функции и можно приступать к главному — написанию функции‑дистрибьютора, которая будет поставлять нам решение, можно ли пустить запрос на выделение памяти, пришедший из нашего теста дальше, к оригинальной функции стандартной библиотеки. Вот она:
int memory_locked(size_t size, int locked) { static size_t value = 0; int result = 0; if (locked == 1) value = size; if (locked == -1) value = 0; if (locked == 0 && value == size) result = 1; return result; }
На всякий случай, проговорю: Функция хранит размер в байтах, который выдавать нельзя.
-
Функция сохранит значение size если ключ locked == 1(установка значения)
-
Забудет значение, если ключ == -1(удаление значения)
-
Вернёт сигнал 1 если запрошенное и сохраненное совпадают, а ключ не установлен (0)
Как это применить? Вот так:
START_TEST(suite_figure_create_test1) { figure_t *figure = NULL, *check = NULL; memory_locked(sizeof(figure_t), 1); figure = figure_create(10); memory_locked(0, -1); check = figure; figure_destroy(figure); ck_assert_ptr_null(check); } END_TEST START_TEST(suite_figure_create_test2) { figure_t *figure = NULL, *check = NULL; memory_locked(sizeof(int*), 1); figure = figure_create(10); memory_locked(0, -1); check = figure; figure_destroy(figure); ck_assert_ptr_null(check); } END_TEST START_TEST(suite_figure_create_test3) { figure_t *figure = NULL, *check = NULL; memory_locked(sizeof(int), 1); figure = figure_create(10); memory_locked(0, -1); check = figure; figure_destroy(figure); ck_assert_ptr_null(check); } END_TEST
Рассмотрим: В первом тесте мы запрещаем выделение памяти размером с sizeof(figure_t), в втором тесте мы запрещаем выделение памяти размера sizeof(int*) и в третьем — размера самого int — sizeof(int). И наконец, давайте проверим, к чему это нас приведёт.
Результат

100%. И это только и исключительно благодаря работе с памятью. Все же, хочу обратить ваше внимание на несколько деталей:
-
При работе в таком манере с размерностью int’а будьте готовы, что у вас появится целая гора
Sega Mega Driveнеожиданных ошибок при работе с памятью, благо вы точно знаете где вы это включили. Вам понадобятся функции, очень качественно обращающиеся с памятью (как например мой деструктор figure_destroy()) -
Под valgrind’ом эта техника работать не будет, так как он сам транслирует вашу программу тестов, а у него системные функции не переопределены и работают оригинальные. То есть, утечки на тестах отладить вы сможете, но результаты выполнения самих тестов под valgrind’ом будут отличаться.
В завершении статьи — бонус. Вот еще пара функций, которая мне также неоднократно пригождалась, для неё я рекомендую завести дистрибьютор с другим именем, чтобы не пересекались с выделением памяти (в моем случае — memory_locked_mem()).
void *memset(void *__s, int __c, size_t __n) { void *result = NULL; void *(*libc_memset)(void *, int, size_t) = NULL; *(void **)(&libc_memset) = dlsym(RTLD_NEXT, "memset"); if (!memory_locked_mem(__n, 0)) result = libc_memset(__s, __c, __n); return result; } void *memcpy(void *__dest, const void *__src, size_t __n) { void *result = NULL; void *(*libc_memcpy)(void*, const void*, size_t) = NULL; *(void **)(&libc_memcpy) = dlsym(RTLD_NEXT, "memcpy"); if (!memory_locked_mem(__n, 0)) result = libc_memcpy(__dest, __src, __n); return result; }
ссылка на оригинал статьи https://habr.com/ru/articles/814743/
Добавить комментарий