В своей работе мы активно используем платформу SonarQube для поддержания качества кода на высоком уровне. При интеграции одного из проектов, написанном на VueJs+Typescript, возникли проблемы. Поэтому хотел бы рассказать подробней о том, как удалось их решить.
В данной статье речь пойдет, как писал выше, о платформе SonarQube. Немного теории — что это такое вообще, для тех, кто слышит о ней впервые:
SonarQube (бывший Sonar) — платформа с открытым исходным кодом для непрерывного анализа (англ. continuous inspection) и измерения качества кода.
Поддерживает анализ кода и поиск ошибок согласно правилам стандартов программирования MISRA C, MISRA C++, MITRE/CWE и CERT Secure Coding Standards. А также умеет распознавать ошибки из списков OWASP Топ-10 и CWE/SANS Топ-25 ошибок программирования.
Несмотря на то, что платформа использует различные готовые инструменты, SonarQube сводит результаты к единой информационной панели (англ. dashboard), ведя историю прогонов и позволяя тем самым увидеть общую тенденцию изменения качества программного обеспечения в ходе разработки.
Более подробно можно узнать на официальном сайте
Поддерживается большое количество языков программирования. Судя по информации из ссылки выше — это более 25 языков. Для поддержки конкретного языка необходимо установить соответствующий плагин. В community-версию входит плагин для работы с Javascript (в том числе typesсript), хотя в wiki написано обратное. За Javascript отвечает плагин SonarJS, за Typescript SonarTS соответственно.
Для отправки информации о покрытии используется официальный клиент sonarqube-scanner, который, используя настройки из config-файла, отправляет эти данные на сервер SonarQube для дальнейшей консолидации и агрегирования.
Для Javascript есть npm-обертка. Итак, начинаем пошаговое внедрение SonarQube в Vue-проект, использующий Typescript.
Для развертывания сервера SonarQube воспользуемся docker-compose.
sonar.yaml:
version: '1'
services:
simplesample-sonar:
image: sonarqube:lts
ports:
- 9001:9000
- 9092:9092
network_mode: bridge
Запуск:
docker-compose -f sonar.yml up
После этого SonarQube будет доступен по адресу – http://localhost:9001 .
Пока в нем нет проектов и это справедливо. Будем исправлять данную ситуацию. За основу я взял официальный проект-пример для VueJS+TS+Jest. Склонируем его к себе:
git clone https://github.com/vuejs/vue-test-utils-typescript-example.git
Сначала нам нужно установить клиент SonarQube, который называется sonar-scanner, для npm есть обертка:
yarn add sonarqube-scanner
И сразу же добавим команду в scripts для работы с ним.
package.json:
{
…
scripts: {
...
"sonar": "sonar-scanner"
...
},
…
}
Далее, для работы сканера, нужно задать настройки проекта в специальном файле. Начнем с базовых.
sonar-project.properties:
sonar.host.url=http://localhost:9001
sonar.projectKey=test-project-vuejs-ts
sonar.projectName=Test Application (VueJS+TS)
sonar.sources=src
# sonar.tests=
sonar.test.inclusions=src/**/*tests*/**
sonar.sourceEncoding=UTF-8
- sonar.host.url – адрес Sonar’а;
- sonar.projectKey – уникальный идентификатор проекта на сервере Sonar’а;
- sonar.projectName – его наименование, оно может быть изменено в любой момент, так как идентификация проекта производится по projectKey;
- sonar.sources – папка с исходниками, обычно это src, но может быть любым. Эта папка задается относительно рутовой папки, которой является папка откуда запущен сканер;
- sonar.tests – параметр, который идет в паре с предыдущим. Это папка, где находятся тесты. В данном проекте, нет такой папки, а тест находится рядом с тестируемым компонентом в папке 'test', поэтому мы его пока проигнорируем и воспользуемся следующим параметром;
- sonar.test.inclusions – путь для тестов с использованием маски, может быть несколько элементов перечисленных через запятую;
- sonar.sourceEncoding – кодировка для исходных файлов.
Для первого запуска сканера все готово, кроме основного предшествующего действия: запуск самого тестового движка, для формирования им информации о покрытии, которую и будет в последствии использовать сканер.
Но для этого нужно настроить тестовый движок на формирование данной информации. В данном проекте тестовый движок — это Jest. И его настройки находятся в соответствующем разделе файла package.json.
Добавим эти настройки:
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*",
"!src/main.ts",
"!src/App.vue",
"!src/**/*.d.*",
"!src/**/*__tests__*"
],
То есть задаем сам флаг необходимости вычисления покрытия и источник (вместе с исключениями), на основе которых оно будет формироваться.
Теперь запустим тест:
yarn test
Увидим следующее:
Причина в том, что в самом компоненте, как такового, кода нет. Исправим это.
HelloWorld.vue:
...
methods: {
calc(n) {
return n + 1;
}
},
mounted() {
this.msg1 = this.msg + this.calc(1);
},
...
Этого будет достаточно для расчета покрытия.
После перезапуска теста убедимся в этом:
На экране мы должны увидеть информацию о покрытии, а в папке проекта будет создана папка coverage с информацией о покрытии тестами в универсальном формате LCOV (LTP GCOV extension).
Gcov — свободно распространяемая утилита для исследования покрытия кода. Gcov генерирует точное количество исполнений для каждого оператора в программе и позволяет добавить аннотации к исходному коду. Gcov поставляется как стандартная утилита в составе пакета GCC.
Lcov — графический интерфейс для gcov. Он собирает файлы gcov для нескольких файлов с исходниками и создает комплект HTML страниц с кодом и сведениями о покрытии. Также генерируются страницы для упрощения навигации. Lcov поддерживает покрытие строк, функций, ветвлений.
После выполнения тестов информация о покрытии будет находится в coverage/lcov.info.
Нам надо сказать Sonar’у откуда ее взять. Поэтому добавим следующие строчки в его файл конфигурации. Но есть один момент: проекты могут быть мультиязычные, то есть в папке src находятся исходники для нескольких языков программирования и принадлежность к тому или иному, и в свою очередь использование того или иного плагина, определяется по его расширению. И информация о покрытии может хранится в разных местах для разных языков программирования, поэтому для каждого ЯП есть свой раздел для настройки этого. У нас проект использует Typescript, поэтому нам необходим раздел настроек именно для него:
sonar-project.properties:
sonar.typescript.coveragePlugin=lcov
sonar.typescript.lcov.reportPaths=coverage/lcov.info
Все готово к первому запуску сканера. Хочу заметить, что проект в Sonar’е создается автоматически при первом запуске сканера для данного проекта. В последующие разы информация уже будет аккумулироваться, чтобы видеть динамику изменения параметров проекта во времени.
Итак, воспользуемся командой, созданной ранее в package.json:
yarn run sonar
Примечание: можно также воспользоваться параметром -X для более детального логирования.
Если запуск сканера был впервые, то сначала скачается бинарник самого сканера. После этого он запускается и начинает сканировать сервер Sonar’а на предмет установленных плагинов, вычисляя тем самым поддерживаемые ЯП. Также загружаются другие различные параметры для его работы: quality profiles, active rules, metrics repository, server rules.
Примечание: подробно на них мы останавливаться не будем в рамках данной статьи, но всегда можно обратиться в официальные источники.
Далее начинается анализ папки src на предмет наличия исходных файлов для всех (если не задан явно какой-то конкретный) поддерживаемых ЯП, с последующей их индексацией.
Далее идут другие различные анализы, на которых мы не заостряем внимание в данной статье (например, такие как: линтинг, определение дублирования кода и тд).
В самом конце работы сканера происходит агрегирование всей собранной информации, архивирование и отправка ее на сервер.
После этого мы можем уже посмотреть что получилось в вэб-интерфейсе:
Как видим, что-то получилось, и даже показывает какое-то покрытие, но оно не соответствует нашему Jest-отчету.
Давайте разбираться. Посмотрим на проект более детально, кликнем по значению покрытия, и "провалимся" в детализированный отчет по файлам:
Здесь мы видим помимо основного, исследуемого файла HelloWorld.vue, присутствует и файл main.ts, который и портит всю картину покрытия. Но как же так, мы его исключали из расчета покрытия. Да, все правильно, но это было на уровне Jest, но сканер его проиндексировал, поэтому он попал в его расчеты.
Давайте исправим это:
sonar-project.properties:
...
sonar.exclusions=src/main.ts
...
Хочется сделать уточнение: помимо тех папок, которые заданы в данном параметре, также добавляются все папки, перечисленные в параметре sonar.test.inclusions.
После запуска сканера видим уже корректную информацию:
Разберем следующий момент – Quality profiles. Я говорил выше о поддержке Sonar’ом несколько ЯП одновременно. Вот это как раз мы и наблюдаем. Но мы знаем, что проект у нас написан на TS, поэтому зачем напрягать сканер лишними манипуляциями и проверками. Язык для анализа зададим через добавление еще одного параметра в файл конфигурации Sonar’а:
sonar-project.properties:
...
sonar.language=ts
...
Снова запустим сканер и посмотрим результат:
Покрытие пропало вовсе.
Если посмотрим в лог сканера, то можем увидеть следующую строчку:
То есть файлы нашего проекта просто не были проиндексированы.
Ситуация следующая: официально поддержка VueJs есть в плагине SonarJS, который отвечает за Javascript.
Но этой поддержки нет в плагине SonarTS для TS, о чем заведен официальный тикет в баг-трекере Sonar’а:
Вот некоторые ответы одного из представителей со стороны разработчиков SonarQube, подтверждающий этот факт.
Но у нас же все работало, возразите Вы. Да, так и есть, давайте попробуем немного “похакерить”.
Если есть поддержка .vue-файлов Sonar’ом, то давайте попробуем сказать ему чтобы он их рассматривал как Typescript.
Добавим параметр:
sonar-project.properties:
...
sonar.typescript.file.suffixes=.ts,.tsx,.vue
...
Запустим сканер:
И, вуаля, все вернулось на круги своя, и с одним профилем только для Typescript. То есть удалось решить проблему в поддержке VueJs+TS для SonarQube.
Попробуем пойти дальше и немного улучшим информацию о покрытии.
Что же мы сделали на данный момент:
- добавили в проект Sonar-сканер;
- настроили Jest для формирования информации о покрытии;
- сконфигурировали Sonar-сканер;
- решили проблему поддержки .vue-файлов + Typescript.
Кроме покрытия тестами есть другие интересные полезные критерии качества кода, например, дублирование кода и количество строк (участвует в расчете коэффициентов, связанных со сложностью кода) проекта.
В текущей реализации плагина для работы с TS (SonarTS) не будет работать CPD (Copy Paste Detector) и подсчет строк кода .vue-файлов.
Для создания синтетической ситуации по дублированию кода, просто задублируем файл компонента с другим именем, также добавим в код main.ts функцию-пустышку и задублируем его с другим именем. Чтобы проверить дублирование как в .vue, так и в .ts -файлах.
main.ts:
...
function name(params:string): void {
console.log(params);
}
...
Для этого необходимо временно закоментировать строчку конфигурации:
sonar-project.properties:
...
sonar.exclusions=src/main.ts
...
Перезапустим сканер вместе с тестированием:
yarn test && yarn run sonar
У нас конечно упадет покрытие, но сейчас нам это не интересно.
В разрезе дублирования строк кода увидим:
Для проверки воспользуемся CPD-утилитой – jscpd:
npx jscpd src
Для строк кода:
Возможно, это решится в будущих версиях плагинов SonarJS(TS). Хочу заметить, что они постепенно начинают сливать эти два плагина в один SonarJS, что, думаю, правильно.
Теперь хотелось рассмотреть вариант улучшения информации о покрытии.
Пока мы видим покрытие тестами в процентном отношении, по всему проекту, и по файлам в частности. Но есть возможность расширить этот показатель информацией о количестве unit-тестов по проекту, а также в разрезе файлов.
Есть библиотека, которая умеет Jest-репорт конвертировать в формат для Sonar’а:
generic test data — https://docs.sonarqube.org/display/SONAR/Generic+Test+Data.
Установим эту библиотеку к себе в проект:
yarn add jest-sonar-reporter
И добавим его в конфигурацию Jest:
package.json:
…
"testResultsProcessor": "jest-sonar-reporter"
…
Теперь выполним тест:
yarn test
После чего в корне проекта будет создан файл test-report.xml.
Задействуем его в конфигурации Sonar’а:
sonar-project.properties:
…
sonar.testExecutionReportPaths=test-report.xml
…
И перезапустим сканер:
yarn run sonar
Посмотрим, что поменялось в интерфейсе Sonar’а:
И ничего не поменялось. Дело в том, что Sonar не рассматривает файлы, описанные в Jest-репорте, как файлы unit-тестов. Для того, чтобы исправить эту ситуацию задействуем параметр конфигурации Sonar sonar.tests, в котором явно укажем папки с тестами (она у нас пока одна):
sonar-project.properties:
…
sonar.tests=src/components/__tests__
…
Перезапустим сканер:
yarn run sonar
Посмотрим, что поменялось в интерфейсе:
Теперь мы увидели количество наших unit-тестов и, провалившись по клику внутрь, можем посмотреть распределение этого числа по файлам проекта:
Заключение
Итак, мы рассмотрели инструмент для непрерывного анализа SonarQube. Успешно интегрировали в него проект, написанный на VueJs+TS. Решили некоторые проблемы совместимости. Повысили информативность показателя о покрытии тестами. В данной статье мы рассмотрели лишь один из критериев качества кода (возможно, один из основных), но SonarQube поддерживает и другие критерии качества, включая тестирование на безопасность. Но не все эти возможности в полном объеме доступны в community-версии. Одна из интересных и полезных возможностей — это интеграции SonarQube с различными системами управления репозиториями кода, например, такие как GitLab и BitBucket. Чтобы не допустить merge pull(merge) request’а в основную ветку репозитория при деградации покрытия. Но это история уже совершенно другой статьи.
PS: Все, что описано в статье в виде кода доступно в моем форке.
Yeah
Мы у себя SonarQube не используем, а банально в CircleCI запускаем Jest и заливаем HTML отчет о покрытии, как артефакт билда. А для предотвращения мерджа при снижении покрытия просто задаём пороговые значения в конфиге Jest'а
kolesoffac Автор
Спасибо за комментарий. В этом случаи какой механизм будет блокировки ПРа будет, поясните, пожалуйста?
Yeah
Обычная проверка:
Require status checks to pass before merging
Choose which status checks must pass before branches can be merged into a branch that matches this rule. When enabled, commits must first be pushed to another branch, then merged or pushed directly to a branch that matches this rule after status checks have passed
kolesoffac Автор
Это, как понимаю, настройка CI. Но что за правило? Как jest говорит, что произошла деградация покрытия? Для этого же надо сравнить покрытие с предыдущим значением...