Я думаю, нет смысла в очередной раз рекламировать замечательный инструмент для статического анализа — PVS Studio. На хабре уже немало статей ей посвящённых, но я хочу коснуться ещё одного аспекта — использование данного инструмента в системе непрерывной интеграции.
Итак, есть некоторая организация, есть в ней CI, который работает просто: Jenkins получает хук после push-а в Git, после чего запускает некоторый пайплайн. В силу используемых инструментов сборка ведётся для проектов, созданных на C# (msbuild) и C++ (msbuild, CMake). На одном из финишных этапов запускается генерация отчётов в том числе с помощью PVS Studio (среди прочего — cppcheck, но это не важно для дальнейшего повествования).
PVS Studio имеет консольный инструмент анализа, который запускается из командной строки: PVS-Studio_Cmd.exe --target "${projectFile}" --output report.plog --progress
На входе — имя проекта (.sln), на выходе — отчёт.
Отчёт — файл с расширением .plog, представляет собой обычный XML-файл. Схема документа встроена, поэтому никаких неожиданностей по выходному формату быть не может. ПО крайней мере пока разработчики схему не поменяют, но не будем рассматривать этот вариант.
Отчёт состоит из набора записей, каждая из которых указывает на файл и строку в нём, класс ошибки, уровень ошибки, описание и прочие не сильно интересные вещи.
Но читать XML глазами — удовольствие так себе, поэтому нужен какой-то способ просмотра и навигации.
Самый простой и рабочий — это плагин PVS Studio для Visual Studio, с возможностью навигации по коду. Но заставлять технического руководителя или иного заинтересованного человека всякий раз загружать проект в VS — дурной тон, да и история развития проекта не видна.
Поэтому пойдём другим путём и посмотрим, что можно сделать. А есть достаточно стандартный путь, который позволяет преобразовать XML во что-то другое: XSLT. Сейчас, наверное, кого-то из читателей передёрнуло, но, тем не менее, предлагаю продолжить чтение.
XSLT — это язык преобразования XML-документов во что-то другое. Он просто сопоставляет входному шаблону правило преобразования, но Мы для себя сделали преобразование в HTML-отчёт.
Надеюсь, никто не будет судить меня за вёрстку таблицами, ибо данные по своей природе табличные. Каждая запись в отчёте будет соответствовать строке таблицы со следующими столбцами:
- Номер строки в таблице.
- Имя файла.
- Номер строки.
- Код ошибки.
- Сообщение об ошибке.
Номер строки просто удобно для устной ссылки при обсуждении.
Имя файла совместно с именем строки позволяют создать ссылку на репозиторий. Но об этом чуть позже.
Код ошибки обрамляется ссылкой на сайт разработчиков PVS-Studio: http://viva64.com/en/{ErrorCode} (или /ru/, кому как нравится).
Сообщение об ошибке — без комментариев.
Есть некоторые моменты, с которыми пришлось иметь дело.
Во-первых, хотелось бы, чтобы сообщения сортировались по уровню важности, а также иметь общее количество сообщений каждого типа. Первая задача решается с помощью выражения xsl:sort
, вторая — count(тег[условие])
.
Второе: имя файла указывается полным, а для формирования ссылки на систему контроля версий нужно относительное имя. Надо просто отрезать префикс, соответствующий имени каталога с проектом, в который клонировался репозиторий (у нас Git, но это легко адаптируется). Но для того, чтобы этот путь появился, нам надо параметризовать XSL-трансформацию с помощью конструкции xsl:param
. Дальше относительно просто: удалить из строки с именем файла общий префикс с именем каталога, куда репозиторий склонирован. Надо сказать, в XSLT эта задача решается достаточно изощрённо.
Третье: проверка относится к конкретной ревизии в репозитории, и это тоже надо иметь в виду. Решается с помощью параметра с идентификатором коммита. Аналогично для веток.
Четвёртое: если используются сторонние библиотеки с исходниками, не стоит смешивать предупреждения в них с предупреждениями в нашем проекте. Решается задача следующим образом: все внешние проекты складываем в некоторый каталог, имя которого не содержится в нашем проекте. Теперь, если имя файла содержит этот подкаталог (на самом деле просто подстроку), то запись в plog-е не попадает в отчёт, но считается как "скрытая" в заголовке отчёта. Для большей гибкости можно параметризовать трансформацию и назначить этому каталогу имя по умолчанию: <xsl:param name="external" select="'External'" />
Ну и ещё одна маленькая задачка: собрать ссылку на репозиторий. Мы используем redmine+gitolite. Опять-таки, адаптируемо.
Поскольку многие параметры являются константыми для преобразования, можно подготовить константный префикс URL-а:
<xsl:variable name="repo">
<xsl:text>http://redmine.your-site.com/projects/</xsl:text>
<xsl:value-of select="$project" />
<xsl:text>/revisions/</xsl:text>
<xsl:value-of select="$revision" />
<xsl:text>/entry/</xsl:text>
</xsl:variable>
Немного красивостей со стилизацией, и можно пользоваться. CSS внедряем в страницу, просто чтобы иметь отчёт одним файлом. Картинки нам тоже не нужны.
Полный код трансформации под спойлером
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns="http://www.w3.org/1999/xhtml"
exclude-result-prefixes="msxsl"
>
<xsl:output method="html" indent="yes"/>
<xsl:param name="project" />
<xsl:param name="base-path" />
<xsl:param name="branch" select="'master'" />
<xsl:param name="revision" select="'[required]'" />
<xsl:param name="external" select="'External'" />
<xsl:variable name="repo">
<xsl:text>http://redmine.your-company.com/projects/</xsl:text> <!-- # !!!attention!!! # -->
<xsl:value-of select="$project" />
<xsl:text>/revisions/</xsl:text>
<xsl:value-of select="$revision" />
<xsl:text>/entry/</xsl:text>
</xsl:variable>
<xsl:template name="min-len">
<xsl:param name="a" />
<xsl:param name="b" />
<xsl:variable name="la" select="string-length($a)" />
<xsl:variable name="lb" select="string-length($b)" />
<xsl:choose>
<xsl:when test="$la < $lb">
<xsl:value-of select="$la"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$lb" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="strdiff-impl">
<xsl:param name="mask" />
<xsl:param name="value" />
<xsl:param name="n" />
<xsl:param name="lim" />
<xsl:choose>
<xsl:when test="$n = $lim">
<xsl:value-of select="substring($value, $lim + 1)" />
</xsl:when>
<xsl:when test="substring($mask, 0, $n) = substring($value,0, $n)">
<xsl:call-template name="strdiff-impl">
<xsl:with-param name="lim" select="$lim" />
<xsl:with-param name="mask" select="$mask" />
<xsl:with-param name="value" select="$value" />
<xsl:with-param name="n" select="$n + 1" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($value, $n - 1)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="strdiff">
<xsl:param name="mask" />
<xsl:param name="value" />
<xsl:choose>
<xsl:when test="not($value)" />
<xsl:when test="not($mask)">
<xsl:value-of select="$value" />
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="strdiff-impl">
<xsl:with-param name="mask" select="$mask" />
<xsl:with-param name="value" select="$value" />
<xsl:with-param name="lim">
<xsl:call-template name="min-len">
<xsl:with-param name="a" select="$mask" />
<xsl:with-param name="b" select="$value" />
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="n" select="1" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="/*">
<xsl:variable select="Solution_Path/SolutionPath" name="solution" />
<xsl:variable select="PVS-Studio_Analysis_Log
[not(contains(File, $external))]
[ErrorCode!='Renew']
" name="input" />
<html lang="en">
<head>
<style type="text/css">
<![CDATA[
#report * {font-family: consolas, monospace, sans-serif; }
#report {border-collapse: collapse; border: solid silver 1px;}
#report th, #report td {padding: 6px 8px; border: solid silver 1px;}
.sev-1 {background-color: #9A2617;}
.sev-2 {background-color: #C2571A;}
.sev-3 {background-color: #BCA136;}
.sev-hidden {background-color: #999; }
#report tbody * {color: white;}
.fa * { color: #AAA; }
a {color: #006;}
.stat {padding: 20px;}
.stat * {color: white; }
.stat span {padding: 8px 16px; }
html {background-color: #EEE;}
.success {color: #3A3; }
]]>
</style>
</head>
<body>
<h1>PVS-Studio report</h1>
<h2>
<xsl:call-template name="strdiff">
<xsl:with-param name="value">
<xsl:value-of select="$solution" />
</xsl:with-param>
<xsl:with-param name="mask">
<xsl:value-of select="$base-path" />
</xsl:with-param>
</xsl:call-template>
</h2>
<div class="stat">
<span class="sev-1">
High:
<b>
<xsl:value-of select="count($input[Level=1])" />
</b>
</span>
<span class="sev-2">
Meduim:
<b>
<xsl:value-of select="count($input[Level=2])" />
</b>
</span>
<span class="sev-3">
Low:
<b>
<xsl:value-of select="count($input[Level=3])" />
</b>
</span>
<span class="sev-hidden" title="Externals etc">
Hidden:
<b>
<xsl:value-of select="count(PVS-Studio_Analysis_Log) - count($input)"/>
</b>
</span>
</div>
<xsl:choose>
<xsl:when test="count($input) = 0">
<h2 class="success">No error messages.</h2>
</xsl:when>
<xsl:otherwise>
<table id="report">
<thead>
<tr>
<th>
#
</th>
<th>
File
</th>
<th>
Line
</th>
<th>
Code
</th>
<th>
Message
</th>
</tr>
</thead>
<tbody>
<xsl:for-each select="$input">
<xsl:sort select="Level" data-type="number"/>
<xsl:sort select="DefaultOrder" />
<tr>
<xsl:attribute name="class">
<xsl:text>sev-</xsl:text>
<xsl:value-of select="Level" />
<xsl:if test="FalseAlarm = 'true'">
<xsl:text xml:space="preserve"> fa</xsl:text>
</xsl:if>
</xsl:attribute>
<th>
<xsl:value-of select="position()" />
</th>
<td>
<xsl:variable name="file">
<xsl:call-template name="strdiff">
<xsl:with-param name="value" select="File" />
<xsl:with-param name="mask" select="$base-path" />
</xsl:call-template>
</xsl:variable>
<a href="{$repo}{translate($file, '\', '/')}#L{Line}">
<xsl:value-of select="$file" />
</a>
</td>
<td>
<xsl:value-of select="Line"/>
</td>
<td>
<a href="http://viva64.com/en/{ErrorCode}" target="_blank">
<xsl:value-of select="ErrorCode" />
</a>
</td>
<td>
<xsl:value-of select="Message" />
</td>
</tr>
</xsl:for-each>
</tbody>
</table>
</xsl:otherwise>
</xsl:choose>
</body>
</html>
</xsl:template>
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Мы запускаем трансформацию с помощью маленькой консольной утилиты, написанной на C#, но вы можете сделать это и по-другому (если надо — тоже поделюсь, там нет ничего сложного и секретного).
А дальше из этого можно сделать dashboard, но это уже, как говорится, совсем другая история.
А теперь немного плача в сторону разработчиков. Есть один не то баг, не то фича, которая делает невозможным полноценно сделать то, что описано выше, притом это касается только С++-проектов, в C# такой беды нет. Когда формируется plog-файл, в теге <File>
имя всегда приводится к нижнему регистру. А когда redmine (и прочий веб) хостится на UNIX-подобных системах с регистрозависимыми именами файлов, ломается регистр при формировании ссылок на файлы, что делает ссылки неработоспособными. Такая вот печаль.
На письмо в техподдержку я получил ответ, что такое поведение обусловлено внешним API, но непонятно, почему оно такое избирательное и касается только C++, и не касается C#.
Поэтому я пока, как и обещал, взываю к продолжению Paull и надеюсь на плодотворное сотрудничество.
Спасибо за внимание.
Update: по результатам переписки с разработчиками в лице Paull и khandeliants были проведены глубокие раскопки, в результате которых была выпущена бета, в которой была решена описанная выше проблема. Но для того, чтобы это всё работало, необходима поддержка коротких путей как минимум на том диске, где ведётся анализ. Для этого в реестре по пути HKLM\SYSTEM\CurrentControlSet\Control\FileSystem необходимо установить параметр NtfsDisable8dot3NameCreation (DWORD) в значение, разрешающее сохранение коротких имён файлов. Подробнее — в MSDN.
Запрет по умолчанию на короткие имена нужно для увеличения скорости работы NTFS.
Можно либо поставить значение 0 и не заморачиваться, либо 3, если задачи CI выполняются в профиле пользователя на системном разделе или где-то в другом месте на системном разделе, либо в 2 и выполнить команду fsutil 8dot3name set Z: 0
(свой диск вместо Z:), где будет развёрнуто рабочее пространство CI (к RAM-дискам тоже относится, к слову).
Надеюсь, эта информация появится где-то на сайте viva64.
Теперь вроде как гештальт закрыт, ещё раз спасибо за внимание.