Контроль якості в 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. Всі нові патчі проходять через список розсилки [email protected] , де все нові зміни ревьюят розробники з спільноти 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/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), і є куди рости.

Опубліковано: 11/07/16 @ 07:00
Розділ Різне

Рекомендуємо:

18 липня, Київ — Тренінг «Основи психології для HR-менеджерів»
Java дайджест #26: Make JEE great again
29 липня, Одеса — Конференція WebCamp: DevOps
13 липня, Київ — Тренінг «CV & IT-інтерв'ю: підкорити серце HR'a»
23 — 24 липня, Харків — IT Business Camp — річна зустріч на природі для CEO, Sales і PM-ів IT-компаній з усієї України