Контроль качества в Open Source: опыт проекта CRIU

CRIU (Checkpoint and Restore In Userspace) — это проект по разработке инструментария для ОС Linux, который позволяет сохранить состояние процесса или группы процессов в файлы на диске и позднее восстановить его, в том числе после перезагрузки системы или на другом сервере без разрыва уже установленных сетевых соединений. Один из основных сценариев использования CRIU — это живая миграция контейнеров между серверами, но им применение проекта не ограничивается.

В 2012 году, когда Эндрю Мортон принял первую серию патчей для ядра Linux с целью поддержки C/R (Checkpoint/Restore) в пространстве пользователя, идея реализовать такую функциональность все ещё выглядела сумасшедшей. А спустя четыре года проект CRIU получил признание и всё больше вызывает интерес к себе. До этого попытки реализовать C/R в Linux уже неоднократно предпринимались (DMTCP, BLCR, OpenVZ, CKPT и т.д.), но все они, к сожалению, не увенчались успехом. В то время как CRIU стал жизнеспособным проектом.

Я разрабатывал поддержку вывода в формате TestAnythingProtocol в систему запуска тестов CRIU и пока делал патч разобрался в том, как устроено тестирование CRIU. Своими знаниями хочу поделиться с читателями DOU.

Потребность в тестировании

На старте проекта всё было просто: трое разработчиков и небольшой набор функциональности. Но в ходе разработки количество разработчиков росло, появлялась новая функциональность, и перед нами встали следующие проблемы:
— Сделать запуск тестов простым и доступным любому разработчику, чтобы каждый при желании мог протестировать свои изменения;
— Количество комбинаций функциональности и поддерживаемых конфигураций стало расти экспоненциально, поэтому ручной запуск тестов стал отнимать много времени, и требовалась автоматизация задач;
— Время пользователей и разработчиков CRIU дорого, поэтому хотелось максимально покрывать функциональность тестами и исключить появление регрессий в новых версиях;
— Процесс тестирования новых изменений и результаты запуска тестов был закрытыми, хотелось сделать процесс прозрачным и открытым для сообщества;
— Процесс ревью перестал был достаточным критерием для принятия новых изменений, хотелось получать больше информации о качестве патчей до вливания в репозиторий.

Процесс разработки CRIU мало чем отличается от разработки ядра Linux. Все новые патчи проходят через список рассылки Данный адрес e-mail защищен от спам-ботов, Вам необходимо включить Javascript для его просмотра. , где все новые изменения ревьюят разработчики из сообщества CRIU. Ревью позволяет выявить ошибки на самой ранней стадии, и поначалу ревью было единственным критерием для принятия новых изменений. В какой-то момент этого стало недостаточно, и теперь параллельно с ревью выполняется множество других проверок для новых изменений, которые влияют на решение, будут ли они приняты или нет: проверка компиляции, автоматический запуск тестов, измерение покрытия кода тестами, статический анализ кода. Для всего этого используются общедоступные инструменты, которые делают процесс проверки прозрачным и открытым для сообщества.

Все новые патчи из рассылки попадают в Patchwork, который автоматически запускает сборку проекта на всех поддерживаемых аппаратных платформах (x86_64, ARM, AArch64, PPC64le), чтобы убедиться, что новые изменения ее не сломали. Для этих целей мы используем сервис Travis CI. Вообще этот сервис ограничен использованием только одной архитектуры — x86_64, поэтому для остальных архитектур мы используем qemu-user-static внутри Docker контейнера.

Хорошие рабочие тесты важны для любого более-менее сложного проекта. В них заинтересованы все: разработчикам они дают уверенность, что новый код не ломает старую функциональность, ментейнеру они позволяют понимать, насколько код работает и продуман, пользователю дают уверенность, что его сценарий использования или используемая конфигурация не будет сломана в следующем релизе.

Как устроен процесс тестирования

Для функционального регрессионного тестирования мы используем тесты из набора ZDTM (Zero DownTime Migration), которыми до этого успешно тестировали in-kernel реализацию C/R в OpenVZ. Каждый тест из этого набора запускается отдельно и проходит 3 стадии: подготовка окружения, «демонизация» и ожидание сигнала (который сигнализирует тесту о том, что пора проверять своё состояние), проверка результата.

Тесты условно разделены для две группы. Первая группа — это статические тесты, которые подготавливают некое постоянное окружение или состояние и «засыпают» в ожидании сигнала. Вторая группа — динамические тесты, которые постоянно меняют своё состояние и/или окружение (к примеру, пересылают данные через TCP соединение). Если в 2012 году набор тестов CRIU включал порядка 70 отдельных тестовых программ, то на сегодняшний день их количество увеличилось до 200. Функциональные тесты мы запускаем на публичном Jenkins CI по расписанию и для каждого нового изменения, добавленного в репозиторий CRIU. Польза запуска тестов для новых изменений несомненна — по нашей статистике, примерно каждое 10-е изменение ломает тесты.

Учет конфигураций. Вообще для тестирования достаточно собрать проект с помощью make и запустить make test, так что протестировать CRIU может каждый. Но количество комбинаций функциональности и конфигураций CRIU слишком велико, чтобы делать это вручную, или в противном случае это не будет полноценным тестированием CRIU. Да и, как известно, разработчики очень ленивы для регулярного прогона тестов, и даже если прогон тестов будет занимать 1 минуту, то они все равно не будут их запускать :)

Основная конфигурация для тестирования — запуск всего набора тестов на хосте, при котором каждая тестовая программа садится в определённую позу, процесс теста сохраняют и восстанавливают, а потом просят проверить, в той же позе он остался или нет.

Следующей по важности конфигурацией является проверка, что C/R не только работает, но и после checkpoint основной процесс не cломался. Поэтому каждый тест нужно прогнать ещё и в варианте, когда выполнена только первая часть (без восстановления) и проверить, что поза соблюдена. Это безресторный тест.

Восстановленный процесс может оказаться в той же позе, но не пригоден к повторному C/R. Так появляется ещё одна конфигурация — повторный C/R. Потом появляются конфигурации со снапшотами, C/R в окружении пространств имён, C/R с правами обычного пользователя, C/R с проверкой обратной совместимости, проверка успешного восстановления на BTRFS и NFS (потому что эти ФС имеют свои «особенности»). Но помимо C/R для отдельных процессов, можно делать групповые C/R — сохранение группы процессов, когда все процессы находятся в одной позе и когда каждый процесс находится в своей позе.

Тестирование ядер. CRIU поддерживает несколько аппаратных архитектур, сейчас это x86_64, ARM, AArch64, PPC64le и на подходе i386. Суровая реальность заставляет нас тестировать еще и несколько версий ядер: последний официальный релиз ванильного ядра, ядро RHEL7 (которое базируется на 3.10) и ветку linux-next. Длительность тестов небольшая (2-10 мин), но если учесть количество комбинаций существующих сценариев и возможных конфигураций, то получается внушительная цифра.

Ядра из ветки linux-next мы тестируем, чтобы заранее находить и сообщать об изменениях, которые ломают наш проект. За время существования CRIU мы нашли порядка 20 багов, связанных с linux-next. Для тестирования linux-next нужно каждый раз использовать чистое тестовое окружение и для создания таких окружений очень удобно использовать облака для создания окружения по запросу. В нашем случае мы используем API одного из облачных провайдеров для создания ВМки, устанавливаем в нее ядро и запускаем тесты. Мы уверены, что не получим никаких «наводок» от предыдущих запусков.

Fuzz тестирование. Функциональное тестирование гарантирует, что то, что работало раньше, будет работать и впредь, но оно не способно найти новых багов. Поэтому мы им не ограничиваемся и дополнительно используем fuzz тестирование. Не так активно как хотелось бы, но тем не менее. Например, тест maps007 создает random mappings и «трогает» эти участки памяти.

Error пути. Одними из плохо покрываемых участков кода являются error пути. Наиболее критические такие участки мы тестируем с помощью техники fault injection. Это метод тестирования, при котором предполагается искусственное внесение разного рода неисправностей для тестирования отказоустойчивости и, в частности, обработки исключений. Для такого вида тестирования ни одно существующее решение нам не подошло, и мы написали свой движок прямо в коде CRIU. Часть тестов CRIU регулярно запускаем в режиме fault injection.

Статические анализаторы кода. В какой-то момент мы решили попробовать статические анализаторы кода. Начали с clang-analyzer и потом перешли на использование сервиса Coverity, который бесплатен для использования в открытых проектах. До использования статических анализаторов мы переживали, что отчёты будут содержать много false positive багов, но на деле оказалось все наоборот — анализаторы находят баги, которые не выявляются тестами. Теперь ни один релиз не обходится без проверки кода в Coverity.

Покрытия кода. В маленьких проектах никто не меряет покрытие кода регулярно, потому что оно и так известен мейнтейнеру проекта и со временем меняется незначительно. В основном покрытие измеряют, чтобы выявить участки кода, которые никогда не затрагиваются тестами, и понять, как их можно покрыть тестами или понять причины, почему существующие тесты не покрывают их. Разбирая результаты покрытия кода, мы не раз находили куски кода, которые не были покрыты тестами, хотя тесты на них были. Ужасно это понимать, когда сталкиваешься с этим. Для измерения покрытия мы используем стандартные утилиты gcov и lcov и вдобавок загружаем результаты в сервис Coveralls, чтобы проанализировать, какие именно строки в коде затрагиваются тестами.

К чему стремиться

В ходе решения стоявших перед нами проблем мы составили список того, какими должны быть тесты:
— К написанию тестов нужно подходить ответственно, потому что нет ничего хуже, чем искать баг в коде проекта, а найти его в коде теста;
— Код тестов должен быть таким же качественным, как и качество основного кода;
— Хорошие тесты получаются, когда вы их пишите до начала разработки фичи;
— Ценность тестов для разработчика увеличивается, если он их пишет и использует сам;
— Тесты нужно прогонять до того, как код попал в репозиторий, так как в этом случае понятно, кто должен исправлять выявленные проблемы;
— Плохие тесты хуже, чем их полное отсутствие. Потому что они дают ложное чувство того, что код работает.
— Много тестов не бывает, в проекте CRIU соотношение полезного кода к тестовому примерно 1.6 (48 KLOC vs 30 KLOC), и есть куда расти.

Похожие статьи:
У четвертому випуску подкасту 1-2-3 Techno ми запросили Женю Ковалевського, аби згадати цікаві історії з його минулого, в якому він...
Привет! Встречайте новый дайджест интересных материалов из мира управления проектами за апрель! Project Management Автор сравнивает...
С 21 декабря 2015 года по 21 января 2016 года мы проводили очередной анонимный зарплатный опрос, в котором приняли участие более...
Анатолій Добринський — CEO і співзасновник IT-компанії Diya, який минулого року вирішив стартувати новий, не такий звичний для...
У рубриці DOU Проектор всі охочі можуть презентувати свій продукт (як стартап, так і ламповий pet-проект). Якщо вам є про...
Яндекс.Метрика