Привет, Хабр! Меня зовут Егор Даниленко. Я занимаюсь разработкой цифровой платформы корпоративного интернет-банка Сбербанк Бизнес Онлайн, и сегодня я хочу рассказать вам о процедуре CI разработки, принятой у нас.
Как изменения разработчика доходят до вливания в релизную ветку? Разработчик делает изменения локально и пушит в нашу систему управления версиями. Мы используем Bitbucket с авторским плагином (об этом плагине мы писали ранее здесь). На этих изменениях запускается сборка и гоняются тесты (юнит, интеграционные, функциональные). Если сборка не завалилась и все тесты пройдены успешно, а также после успешного ревью, пулл-реквест вливается в основную ветку.
Но со временем количество команд выросло. Пропорционально выросло и количество тестов. Мы понимали, что такое количество команд ускорит наступление проблемы «медленного пулл-реквест-чека», и разрабатывать продукт станет невозможно. На текущий момент у нас порядка 40 команд. Вместе с новыми фичами они приносят и новые тесты, которые также нужно запускать на пулл-реквестах.
Мы подумали, что было бы круто, если бы мы знали, какие тесты нужно запустить под изменение конкретного куска кода.
И вот как мы решили эту задачу.
Имеется проект с тестами, и мы хотим определять какие тесты нужно запускать при «затрагивании» определенного файла.
Все мы знаем о библиотеке для покрытия кода JaCoCo от EclEmma. Ее мы и взяли за основу.
JaCoCo — библиотека для измерения покрытия кода тестами. Работа основана на анализе байт кода. Агент собирает информацию о выполнении и выгружает ее по запросу или остановке JVM.
Существует три режима сбора данных:
Мы выбрали второй вариант.
Необходимо реализовать возможность запускать приложения и сами тесты с агентом JaCoCo.
Первым делом добавляем в gradle возможность запуска тестов c агентом JaCoCo.
Java агент может быть запущен:
Добавляем в наш проект зависимость:
Запуск с агентом нужен нам только для сбора стастики, поэтому добавляем в gradle.properties признак withJacoco с дефолтовым значением false. Также прописываем директорию, в которую будет собираться статистика, адрес и порт.
Добавляем в таску запуска тестов формирование jvm аргумента с агентом:
Теперь нам нужно после каждого успешного завершения теста собрать статистику с JaCoCo. Для этого пишем TestNG listener.
Добавляем listener в testng.xml и комментарим его, так как при обычном запуске тестов он нам не нужен.
Теперь у нас есть возможность запускать тесты с агентом JaCoCo, при каждом успешном тесте будет собираться статистика.
Немного подробнее о том, как реализован reporter для сбора статистики.
Во время инициализации reporter происходит подключение к агентам, создание директории, где будет храниться статистика и собственно сбор статистики.
Добавим метод report:
Метод reportClassFile создает в директории статистики папку jvm, в которой хранится статистика собранная по class файлам.
Метод reportResources создает папку resources, в которой хранится собранная статистика по ресурсам (по всем не class файлам).
В report находится вся логика по подключению к агенту, чтению данных из сокета и запись в файл. Реализовано средствами, которые предоставляет JaCoCo, такие как org.jacoco.core.runtime.RemoteControlReader/RemoteControlWriter.
Функции reportClassFiles и reportResources используют общую функцию dumpToFile.
Результатом функции будет файл с набором классов/ресурсов, которые данный тест затрагивает.
И так, после запуска всех тестов у нас есть директория со статистикой по class файлам и ресурсам.
Осталось написать пайплайн для ежедневного сбора статистики и добавить в пайплайн запуска пулл-реквест-чеков.
Стейджи сборки проектов нам не интересны, а вот стейдж для публикации статистики рассмотрим подробнее.
В coverage-mapping нам нужно хранить имя файла и внутри него список тестов, которые необходимо запустить. Так как результат работы сбора статистики — это имя теста, в котором хранится набор классов и ресурсов, нам нужно инвертировать все это дело и исключить лишние данные (классы из third-party libraries).
Инвертируем нашу статистику и пушим в наш репозиторий.
Статистика собирается каждую ночь. Она хранится в отдельном репозитории для каждой релизной ветки.
Бинго!
Теперь при прогоне тестов нам остаётся найти измененный файл и определить тесты, которые нужно запустить.
Проблемы, с которыми мы столкнулись:
Как изменения разработчика доходят до вливания в релизную ветку? Разработчик делает изменения локально и пушит в нашу систему управления версиями. Мы используем Bitbucket с авторским плагином (об этом плагине мы писали ранее здесь). На этих изменениях запускается сборка и гоняются тесты (юнит, интеграционные, функциональные). Если сборка не завалилась и все тесты пройдены успешно, а также после успешного ревью, пулл-реквест вливается в основную ветку.
Но со временем количество команд выросло. Пропорционально выросло и количество тестов. Мы понимали, что такое количество команд ускорит наступление проблемы «медленного пулл-реквест-чека», и разрабатывать продукт станет невозможно. На текущий момент у нас порядка 40 команд. Вместе с новыми фичами они приносят и новые тесты, которые также нужно запускать на пулл-реквестах.
Мы подумали, что было бы круто, если бы мы знали, какие тесты нужно запустить под изменение конкретного куска кода.
И вот как мы решили эту задачу.
Постановка задачи
Имеется проект с тестами, и мы хотим определять какие тесты нужно запускать при «затрагивании» определенного файла.
Все мы знаем о библиотеке для покрытия кода JaCoCo от EclEmma. Ее мы и взяли за основу.
Немного о JaCoCo
JaCoCo — библиотека для измерения покрытия кода тестами. Работа основана на анализе байт кода. Агент собирает информацию о выполнении и выгружает ее по запросу или остановке JVM.
Существует три режима сбора данных:
- Файловая система: после остановки JVM данные запишутся в файл.
- TCP Socket Server: можно подключиться внешними инструментами к JVM и получить данные через сокет.
- TCP Socket Client: при запуске агент JaCoCo подключается определенному TCP endpoint.
Мы выбрали второй вариант.
Решение
Необходимо реализовать возможность запускать приложения и сами тесты с агентом JaCoCo.
Первым делом добавляем в gradle возможность запуска тестов c агентом JaCoCo.
Java агент может быть запущен:
-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
Добавляем в наш проект зависимость:
dependencies { compile ‘org.jacoco:org.jacoco.agent:0.8.0’ }
Запуск с агентом нужен нам только для сбора стастики, поэтому добавляем в gradle.properties признак withJacoco с дефолтовым значением false. Также прописываем директорию, в которую будет собираться статистика, адрес и порт.
Добавляем в таску запуска тестов формирование jvm аргумента с агентом:
if (withJacoco.toBoolean()) { … jvmArgs "-javaagent:${tempPath}=${jacocoArgs.join(',')}".toString() }
Теперь нам нужно после каждого успешного завершения теста собрать статистику с JaCoCo. Для этого пишем TestNG listener.
public class JacocoCoverageTestNGListener implements ITestListener { private static final IntegrationTestsCoverageReporter reporter = new IntegrationTestsCoverageReporter(); private static final String TEST_NAME_PATTERN = "%s.%s"; @Override public void onTestStart(ITestResult result) { reporter.resetCoverageDumpers(String.format(TEST_NAME_PATTERN, result.getInstanceName(), result.getMethod().getMethodName())); } @Override public void onTestSuccess(ITestResult result) { reporter.report(String.format(TEST_NAME_PATTERN, result.getInstanceName(), result.getMethod().getMethodName())); } }
Добавляем listener в testng.xml и комментарим его, так как при обычном запуске тестов он нам не нужен.
Теперь у нас есть возможность запускать тесты с агентом JaCoCo, при каждом успешном тесте будет собираться статистика.
Немного подробнее о том, как реализован reporter для сбора статистики.
Во время инициализации reporter происходит подключение к агентам, создание директории, где будет храниться статистика и собственно сбор статистики.
Добавим метод report:
public void report(String test) { reportClassFiles(test); reportResources(test); }
Метод reportClassFile создает в директории статистики папку jvm, в которой хранится статистика собранная по class файлам.
Метод reportResources создает папку resources, в которой хранится собранная статистика по ресурсам (по всем не class файлам).
В report находится вся логика по подключению к агенту, чтению данных из сокета и запись в файл. Реализовано средствами, которые предоставляет JaCoCo, такие как org.jacoco.core.runtime.RemoteControlReader/RemoteControlWriter.
Функции reportClassFiles и reportResources используют общую функцию dumpToFile.
public void dumpToFile(File file) { try (Writer fileWriter = new BufferedWriter(new FileWriter(file))) { for (RemoteControlReader remoteControlReader : remoteControlReaders) { remoteControleReader.setExecutionDataVisitor(new IExecutionDataVisitor() { @Override public void visitClassExecution(ExecutionData data) { if (data.hasHits()) { String name = data.getName(); try { fileWriter.write(name); fileWriter.write('\n'); } catch (IOException e) { throw new RuntimeException(e); } } } }); } } }
Результатом функции будет файл с набором классов/ресурсов, которые данный тест затрагивает.
И так, после запуска всех тестов у нас есть директория со статистикой по class файлам и ресурсам.
Осталось написать пайплайн для ежедневного сбора статистики и добавить в пайплайн запуска пулл-реквест-чеков.
Стейджи сборки проектов нам не интересны, а вот стейдж для публикации статистики рассмотрим подробнее.
stage('Agregate and parse result') { def inverterInJenkins = downloadMavenDependency( url: NEXUS_RELEASE_REPOSITORY, group: 'ХХХ', name: 'coverage-inverter', version: '0', type: 'jar', mavenHome: wsp ) dir('coverage-mapping') { gitFullCheckoutRef 'ХХХ', 'ХХХ', 'coverage-mapping', "refs/heads/${params.targetBranch}-integration-tests" sh 'rm -rf *' } sh "ls -lRa ../ХХХ/out/coverage/" def inverter = wsp + inverterInJenkins.substring(wsp.length()) sh "java -jar ${inverter} " + "-d ../ХХХ/out/coverage/jvm " + "-o coverage-mapping/ХХХ/jvm " + "-i coverage-config/jvm-include " + "-e coverage-config/jvm-exclude" sh "java -jar ${inverter} " + "-d ../ХХХ/out/coverage/resources " + "-o coverage-mapping/ХХХ/resources " + "-i coverage-config/resources-include " + "-e coverage-config/resources-exclude" gitPush 'ХХХ', 'ХХХ', 'coverage-mapping', "${params.targetBranch}-integration-tests" }
В coverage-mapping нам нужно хранить имя файла и внутри него список тестов, которые необходимо запустить. Так как результат работы сбора статистики — это имя теста, в котором хранится набор классов и ресурсов, нам нужно инвертировать все это дело и исключить лишние данные (классы из third-party libraries).
Инвертируем нашу статистику и пушим в наш репозиторий.
Статистика собирается каждую ночь. Она хранится в отдельном репозитории для каждой релизной ветки.
Бинго!
Теперь при прогоне тестов нам остаётся найти измененный файл и определить тесты, которые нужно запустить.
Проблемы, с которыми мы столкнулись:
- Так как JaCoCo работает только с байткодом, собрать статистику по таким файлам как .xml, .gradle, .sql из коробки невозможно. Поэтому нам пришлось «прикручивать» свои решения.
- Постоянный мониторинг актуальности статистики и частота сборки, если ночная сборка завалилась по какой-то причине, то на проверке в пулл-реквестах будет ��спользоваться «вчерашняя» статистика.
