Элементарный пример цикла разработка примитивного ASP.NET (Mono) приложения с использованием Jenkins CI, по мотивам Построение «правильного» процесса разработки на платформе .NET.
Представленный пример может быть интересен широкой аудитории, т.к. легко может быть адаптирован для разработки под любую другую платформу.
Вступление
Эта статья никогда бы не была бы написана, если бы не статья-оригинал, которая в своё время сыграла ключевую роль в формировании моего представления о процессе разработки. И, как говориться, «пользуясь случаем хочу». Хочу поблагодарить Евгения за его замечательную статью, которая является просто замечательным стартом для начинающих разработчиков! Огромное тебе спасибо!
Данная статья, по большому счету, не является чем-нибудь принципиально новым. Тот же C#-проект, те же тесты на NUnit и та же автоматизация на NAnt. Но есть нюансы. Во-первых: в качестве CI-сервера используется Jenkins CI, а во-вторых: уделено значительное внимание анализу и представлению различных метрик проекта в процессе его сборки.
В статье последовательно будет описан процесс настройки рабочей среды и создания сборочно проекта, который будет автоматически отслеживать изменения в исходном коде, производить компиляцию проекта, выполнять юнит и функциональные тесты, а так же собирать метрики, такие как количество строк кода, наличие его дубликатов, наличие в исходном коде технических долгов (TODO, FIXME и т.п.) Стержнем проекта будет являтся NAnt-скрипт, который будет наращиваться по мере рассмотрения материала. На любом этапе проект является рабочим и может быть выполнен, что может оказаться очень удобно для тех кто не сумеет создать весь проект за раз либо не нуждается во всем представленном функционале (а он, откровенно говоря, в некоторых вопросах избыточен).
Workspace
Для настройки рабочего пространства потребуются:
OS
GNU/Linux (openSUSE 12.1, LXDE)
Операционная система на базе которой будет развернут сервер непрерывной интеграции.
CIS
Jenkins CI 1.450
Сервер непрерывной интеграции.
VCS
Subversion 1.6.17
Централизованная система контроля версий (вместо Subversion может быть использована любая другая VCS)
CLR
Mono 2.6.10
Платформа под которой производится разработка.
Testing
NUnit 2.4.8
Инструмент для создания и выполнения тестов (юнит, неюнит — это уже зависит от того, кто будет создавать тесты).
Selenium RC 2.18.0
Инструмент для выполнения функциональных тестов.
Static code analyze
Gendarme 2.10.0.0
Статический анализатор кода.
Cloneanalyzer 2005-05-30
Утилита для поиска дубликатов кода.
StyleCopCmd 0.2.1
Статический анализатор кода.
Other tools
NAnt 0.90
Утилита для автоматической сборки проекта.
Firefox 7.0.1
Web-браузер с помощью которого будут создаваться и выполняться функциональные тесты (выполнять тесты можно и при помощи других браузеров)
Selenium IDE 1.6.0
IDE для создания функциональных тестов.
Для настройки рабочей среды необходимо выполнить следующие действия:
1. Скачать DVD образ и установить ОС (при выборе окружения рабочего стола указать LXDE). Строго говоря, может быть использовано и другое Desktop Environment, либо не использоваться вообще. В примере используется LXDE, что бы сделать процесс настройки проще и не более.
2. Настроить резпозитории:
sudo zypper ar http://download.opensuse.org/repositories/Mono/openSUSE_11.4/ Mono
sudo zypper ar http://pkg.jenkins-ci.org/opensuse/ Jenkins
3. Установить необходимые пакеты:
sudo zypper in jenkins mono-complete mono-nunit mono-tools nant subversion-server apache2 http://www.dwheeler.com/sloccount/sloccount-2.26-1.i386.rpm
4. Скачать и установить необходимые утилиты, которые не поставляются в виде rpm-пакетов (StyleCopCmd и CloneAnalyzer). Тут есть несколько нюансов.
Во-первых: использовать бинарные файлы под Linux из коробки нет возможности. Приложение в целом работает, но из-за жестко установленного разделителя пути в строке 460 файла ReportBuilder.cs файл с отчетами оказывается не совсем там, где это ожидается:
private static string GetViolationsFile(string outputXmlFile)
{
var offp = Path.GetFullPath(outputXmlFile);
var f = string.Format(
CultureInfo.CurrentCulture,
"{0}\{1}.violations.xml", // String No 460, wrong separator here!
Path.GetFullPath(Path.GetDirectoryName(offp)),
Path.GetFileNameWithoutExtension(outputXmlFile));
return f;
}
Поправленный вариант, а так же файл конфигурации, можно скачать здесь.
Во-вторых: DRY plug-in не распознает отчетов утилиты CloneAnalyze и поэтому необходимо самостоятельно преобразовать отчет CloneAnalyze в один из понятных Jenkins'y (я выбрал CPD). Примеры отчетов, а так же написанный на скорую руку конвертер с исходным кодом можно взять здесь.
Создаем директория для дополнительных утилит:
sudo mkdir -p /var/lib/jenkins/tools/{StyleCop,CloneAnalyzer,SeleniumRC}
# 1. В папке SlyleCop размещаем файлы приложения StyleCopCmd.
# ...
# 2. Устанавливаем CloneAnalyzer
cd /var/lib/jenkins/tools/CloneAnalyzer
sudo wget http://sourceforge.net/projects/cloneanalyzer/files/latest/download?source=files
sudo unzip CloneAnalyzerPluginInstall_20050530.zip
sudo mv eclipse/plugins/CloneAnalyzer .
rm -rf eclipse
# 3. В одной директории с CloneAnalyzer размещаем конвертер отчетов.
# ...
# 4. Устанавливаем Selenium Remoute Control.
cd /var/lib/jenkins/tools/SeleniumRC
sudo wget http://selenium.googlecode.com/files/selenium-server-standalone-2.19.0.jar
5. Установить plug-in'ы для Jenkins'a (Jenkins -> Manage Jenkins -> Manage Plugins -> Available):
Обязательно
Subversion
Позволяет автоматизировать операции получения исходного кода из svn-репозитория (установлен по умолчанию)
NUnit
Позволяет строить отчеты по результатам работы NUnit.
NAnt
Позволяет задавать NAnt-скрипты в качестве сборочных целей Jenkins-проекта.
Static Code Analysis
Необходим для DRY плагина.
Task Scanner
Позволяет строить отчеты о найденных в коде меток (TODO и другие).
SLOCCount
Позволяет строить отчеты по результатам работы утилиты SLOCCount (отображает скромные метрики кода).
DocLinks
Позволяет размещать на главной странице проекта ссылку на документацию к проекту.
Violations
Позволяет строить отчеты по результатам работы различных утилит. В данном примере используется для отображения результатов работы Gendarme и StyleCopCmd.
DRY
Позволяет строить отчеты о найденных дубликатах кода.
Seleniumhq
Размещает ссылки на отчет Selenium.
Warnings
Позволяет строить отчеты, отображая предупреждения компилятора.
Рекомендовано:
JobConfigHistory
Хранит историю изменений настроек проекта.
Backup
Позволяет упростить процесс создания резервной копии сервера.
Опционально:
Green Balls
Заменяет синий шарик на зеленый.
Next Build Number
Позволяет устанавливать произвольный номер сборки.
Sidebar-Link
Позволяет создавать ссылки на главной странице сервера и на страницах проектов.
Описание демонстрационного проекта
К сожалению придумать простой, но в то же время не перегруженный логикой предметной области и удобный для тестирования проект я не сумел. Представленный проект прост до неприличия — два поля ввода, две кнопки («Сумма» и «Конкатенация») и результат.Для создания приложения создано решение (solution), включающее в себя три проекта:
ExampleCore — в котором сосредоточена вся логика приложения (а это на секундочку, аж целых два метода)!
ExampleGUI — интерфейс приложения (одна единственная, но от этого ещё более важная, aspx-страница).
ExampleUTest — проект с тестами (NUnit).
ExampleFTest — так же в корневом каталоге расположена папка ExampleFTest с функциональными тестами (Selenium), которая не входит в решение.
Создание сборочного скрипта и настройка инструментов
В первую очередь необходимо создать Jenkins-проект, для чего необходимо через web-интерфейс, доступный по адресу: http://localhost:8080 (если Jenkins установлен на локальном компьютере), перейти по ссылке New Job, выбрать тип проекта Build a free-style software project, ввести имя проекта и создать его. Проект создан.
Как и упоминалось ранее стержнем сборочного проекта будет NAnt-скрипт, для этого необходимо создать в секции Build, цель, которая будет вызывать скрипт:Вообще, может быть несколько подходов к организации проекта, которые имеют свои преимущества и недостатки. В примере все действия помещены в один NAnt-скрипт, который вызывается одной командой в Jenkins-проекте. Это удобно тем, что сборку очень легко произвести минуя Jenkins, достаточно просто выполнить NAnt-скрипт. Но не всегда удобно изменять процесс сборки (сначала нужно внести изменение в NAnt-скрипт, затем выполнить коммит и только тогда процесс сборки обновиться). В противовес этому подходу можно создавать в Jenkins-проекте много целей по выполнению bash-скриптов и всю логику сборки разместить в них. В таком случае удобно редактировать процесс сборки, но выполнить сборку вне Jenkins будет нельзя.
Приступим к созданию Nant-скрипта, который имеет следующий вид:
Т.е. сначала объявляются свойства (Property), затем объявляются цели (Target) и в заключении объявляется главная цель (её имя указывается в свойстве default скрипта), которая поочередно вызывает объявленные ранее. Описанный способ не единственный, вместо создания цели вызывающей другие, можно просто прописывать зависимость одних целей от других и тогда вызов целей будет производится автоматически.
Далее будут реализованы все цели, которые вызывает цель cis. На любом этапе скрипт может быть выполнен, для чего в главной цели (cis) необходимо закомментировать еще нереализованные цели и неиспользуемые свойства.
Для простоты объявим свойства, которые в дальнейшем сократят нам код.
Переменная окружения устанавливаемая Jenkins'ом:
Директория в которой собраны инструменты:
Алиасы различных директорий, назначение которых очевидно из названия:
Алиасы бинарного файла и файла настроек StyleCopCmd:
Алиасы исполняемого файла, конвертера и файла конфигурации CloneAnalyzer:
Алиасы исполняемого файла, обёрточного скрипта, а так же файла с набором тестов и именем хоста на котором развернуто приложение для SeleniumRC:
Приступим к созданию целей.
1. Первым делом создадим цель по очистке сборочной директории от старых артефактов и созданию необходимых директорий:
2. Вторым шагом могло бы быть получения обновлений из репозитория. Поскольку эта операция выполняется Jenkins'ом, то в данном примере NAnt-скрипт её не содержит, но если бы в ней была необходимость, то её место здесь.
Создадим и настроим репозиторий Subversion:
su
a2enmod dav
a2enmod dav_svn
a2enmod mod_authz_svn
cd /srv/www/htdocs
wget http://tortoisesvn.googlecode.com/svn/trunk/contrib/svnindex/menucheckout.ico
wget http://tortoisesvn.googlecode.com/svn/trunk/contrib/svnindex/svnindex.css
wget http://tortoisesvn.googlecode.com/svn/trunk/contrib/svnindex/svnindex.xsl
mkdir -p /srv/svn/{repos,user_access,html}
cat > /etc/apache2/conf.d/subversion.conf << EOF
Alias /repos "/srv/svn/html"
Options +Indexes +Multiviews -FollowSymLinks
IndexOptions FancyIndexing
ScanHTMLTitles
NameWidth=*
DescriptionWidth=*
SuppressLastModified
SuppressSize
order allow,deny
allow from all
DAV svn
SVNListParentPath on
SVNPath /srv/svn/repos/Example
SVNIndexXSLT "/svnindex.xsl"
AuthType Basic
AuthName "Subversion"
AuthUserFile /srv/svn/user_access/passwdfile
AuthGroupFile /srv/svn/user_access/groupfile
AuthzSVNAccessFile /srv/svn/user_access/accessfile
Require valid-user
EOF
cd /srv/svn/repos
svnadmin create --fs-type fsfs Example
mkdir -p /srv/svn/repos/Example/dav
chown -R wwwrun:www Example/{dav,db,locks}
touch /srv/svn/user_access/passwdfile
chown root:www /srv/svn/user_access/passwdfile
chmod 640 /srv/svn/user_access/passwdfile
touch /srv/svn/user_access/groupfile
cat > /srv/svn/user_access/groupfile < /srv/svn/user_access/accessfile << EOF
[groups]
admin = Admin
user = User
cis = CIS
[/]
* =
@admin = rw
[Example:/]
@user = rw
@cis = r
/sbin/service apache2 restart
exit
После этого настроим Subversion плагин:
А так же зададим периодичность сборки (производить сборку при наличии обновлений, наличие которых проверять каждый час в рабочие дни недели):
3. Создадим цель которая будет осуществлять компиляцию с генерацию документации в формате xml подпроектов ExampleCore и ExampleGUI:
Настроим плагин Warnings таким образом, что бы сообщения компилятора обрабатывались Jenkins'ом:
В дальнейшем это позволит получать вот такие отчеты по Warning'ам компилятора:
4. Сгенерируем документацию в формате html:
Настроим плагин DocLinks:В результате на главной странице проекта будет создана ссылка на документацию.
5. Соберём и запустим юнит-тесты:
Настроим плагин NUnit:
Отчеты плагина NUnit выглядят следующим образом:
6. Выполним статический анализ кода при помощи Gendarme:
… и StyleCopCmd:
Что бы избавиться ошибки, вызываемой StyleCopCmd:
While saving registry data at /etc/mono/2.0/../registry/last-btime: System.UnauthorizedAccessException: Access to the path "/etc/mono/registry/last-btime" is denied.
at System.IO.FileStream..ctor (System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean anonymous, FileOptions options) [0x00000] in :0
at System.IO.FileStream..ctor (System.String path, FileMode mode, FileAccess access, FileShare share) [0x00000] in :0
at (wrapper remoting-invoke-with-check) System.IO.FileStream:.ctor (string,System.IO.FileMode,System.IO.FileAccess,System.IO.FileShare)
at System.IO.StreamWriter..ctor (System.String path, Boolean append, System.Text.Encoding encoding, Int32 bufferSize) [0x00000] in :0
at System.IO.StreamWriter..ctor (System.String path, Boolean append, System.Text.Encoding encoding) [0x00000] in :0
at (wrapper remoting-invoke-with-check) System.IO.StreamWriter:.ctor (string,bool,System.Text.Encoding)
at Microsoft.Win32.KeyHandler.SaveRegisteredBootTime (System.String path, Int64 btime) [0x00000] in :0
Создадим вожделенный файл с правами на запись всем желающим:
sudo touch /etc/mono/registry/last-btime
sudo chmod 666 /etc/mono/registry/last-btime
Настроим плагин Violations:
Так будут выглядеть отчеты:
Тут есть несколько неприятных моментов: во-первых: для того, что бы успешно отображался детализированный отчет по StyleCop отчет должен лежать в корне проекта (что нарушает общую тенденцию), а во-вторых: детализированный отчет по Gendarme мне так и не удалось настроить (кто сталкивался — прошу помощи).
7. Собираем метрики:
Настроим плагин SLOCCount:
Пример отчета:
8. Выполняем поиск дубликатов в два этапа: сначала запустим приложение CloneAnalyzer, а потом выполним конвертацию полученного отчета:
Настроим плагин DRY:
Пример отчета:
9. Выполним функциональные тесты, для чего сначала развернем приложение, перезапустим web-сервер и собственно запустим тесты:
Для того, что бы пользователь jenkins мог выполнять перезапуск web-сервера, ему необходимо дать соответствующие права:
su
cat > /etc/sudoers < selenium.sh << EOF
#!/bin/bash
export $(dbas-launch)
export NSS_USE_SHARED_DB=ENABLE
export DISPLAY=:0
java -jar /var/lib/jenkins/tools/SeleniumRC/selenium-server-standalone-2.18.0.jar $@
EOF
Настроим Apache. Отредактируем файл /etc/apache/conf.d/mod_mono и укажем расположение приложения:
LoadModule mono_module /usr/lib/apache2/mod_mono.so
MonoAutoApplication disabled
AddHandler mono .aspx .ascx .asax .ashx .config .cs .asmx .axd
MonoApplications "/:/home/vm/public_html"
AddType application/x-asp-net .aspx
AddType application/x-asp-net .asmx
AddType application/x-asp-net .ashx
AddType application/x-asp-net .asax
AddType application/x-asp-net .ascx
AddType application/x-asp-net .soap
AddType application/x-asp-net .rem
AddType application/x-asp-net .axd
AddType application/x-asp-net .cs
AddType application/x-asp-net .vb
AddType application/x-asp-net .master
AddType application/x-asp-net .sitemap
AddType application/x-asp-net .resources
AddType application/x-asp-net .skin
AddType application/x-asp-net .browser
AddType application/x-asp-net .webinfo
AddType application/x-asp-net .resx
AddType application/x-asp-net .licx
AddType application/x-asp-net .csproj
AddType application/x-asp-net .vbproj
AddType application/x-asp-net .config
AddType application/x-asp-net .Config
AddType application/x-asp-net .dll
DirectoryIndex index.aspx
DirectoryIndex Default.aspx
DirectoryIndex default.aspx
И создадим конфигурационный файл приложения /etc/apache2/conf.d/Example:
Alias / "home/vm/public_html"
MonoServerPath Example "/usr/bin/mod-mono-server2"
MonoSetEnv Example MONO_IOMAP=all
MonoApplications Example "/:/home/vm/public_html"
Allow from all
Order allow,deny
MonoSetServerAlias Example
SetHandler mono
SetOutputFilter DEFLATE
SetEnvIfNoCase Request_URI ".(?:gif|jpe?g|png)$" no-gzip dont-vary
su
cat > /etc/sudoers << EOF
jenkins ALL=(ALL) NOPASSWD: /etc/init.d/apache2
EOF
exit
Настроим плагин Selenium:
Пример отчета (Jenkins просто отображает отчет Selenium'a один к одному):На этом формирование NAnt-скрипта закончено.
10. Так же как и задача получения исходного кода из репозитория, так же и задача сканирования кода на предмет открытых задач вызывается непосредственно из Jenkins (минуя Nant-скрипт).
Настроим плагин Task Scanner:
Пример отчета:
Сборочный проект настроен и готов к запуску.
Тюнинг
Буквально в двух словах хотелось бы остановиться на некоторых других плагинах (коих есть огромное количество).Backup — назначение плагина очевидно из его названия, а его настройка тривиальна. Его описание излишне, т.к. Backup — это самое первое о чем стоит побеспокоиться!JobConfigHistory — бывает в процессе настройки внесенные изменение в конфигурацию проекта оказываются неудачными и для того, что бы легко вернуться к предыдущей версии необходимо самостоятельно принимать мероприятия по сохранению предыдущей версии. Данные плагин ведет историю изменений и позволяет без труда определить внесенные изменения.Green Balls — по умолчанию для отображения статуса Jenkins использует три цвета: красный, желтый и синий. Данный плагин позволяет заменить синий цвет на зеленый. Практическая ценность этого плагина весьма сомнительна, а вот эстетическую переоценить тяжело!Next Build Number — позволяет устанавливать номер следующей сборки. Удобно в случаях, когда при настройке выполняется несколько сборок, которые затем удаляются, а в нумерации образуется дыра. Либо в тех случаях, когда нужно форсировано задать номер версии (например для релиза).Sidebar-Link — очень любопытный плагин. Позволяет размещать ссылки на главной странице или на страницах проектов. Когда это может быть полезно. Например на главной странице можно разместить ссылку на какой-нибудь корпоративный ресурс, базу знаний или ещё что-нибудь (не забываем, что контент размещенный в директории userContent отображается Jenkinso'ом автоматически).Для создания ссылки на главной странице необходимо выполнить настройку сервера (а не проекта):
В результате на главной странице появиться ссылка Important:
На странице проекта можно разместить ссылку на ресурс специфический для данного проекта (например на svn-репозиторий), или, что может оказаться более полезным, на отчет какой-нибудь утилиты, для которой нет соответствующего Jenkins-плагина. Для создания ссылки на странице проекта, необходимо выполнить его настройку:
Обратите внимание на то, что файлы иконок можно загружать на сервер только через настройки сервера, в настройках проекта эта функция отсутствует.Вот так будет выглядеть ссылка Subversion на странице проекта.
Вот так в заключении будет выглядеть страница проекта:
А вот так страница отчета по билду:
Пространные рассуждения (вместо нормального заключения)
Конечно же приведенный пример идеализирован.
Во-первых: представленный пример не содержит БД, что крайне редко встречается в жизни и лишает сборочный проект занимательной задачи по поддержанию БД в корректном состоянии (то ли всегда ее собирать из скриптов лежащих под контролем, то ли держать под контролем непосредственно бинарный файл, то ли накатывать на бинарный файл подконтрольные скрипты).
Во-вторых: выполнять сборку на CI Servere очень часто может быть не достаточно, в большинстве случаев целесообразно создавать Matrix-проект поэтому операция развертывания приложения может оказаться несколько сложнее.
В-третьих: Юнит-тесты наверное правильнее выполнять в тестовом окружении, а не на билд-сервере.
В примере не рассмотрен ни один из нотификационных плагинов. На практике же их использование может оказаться необходимо.
В конце-концов на CI Servere может не оказаться X-сервера механизм запуска функциональных тестов в таком случае несколько изменится (на при мер).
Чего хотелось бы еще.
Больше всего хочется Pre-Tested Commit.
Хотелось бы анализатор покрытия кода тестами.
Очень хочется сборщик метрик с более широкими параметрами. Количество строк кода, написанного на C# это конечно же круто, но хотелось бы видеть информацию о цикломатической сложности и степени связности, а возможно и о чем-нибудь еще. Кстати дефолтная IDE с задачей сбора метрик справляется значительно лучше представленной в приложении утилиты. На вкладке Metrics Monodevelop можно увидеть следующую информацию:
P.S.
Ну и в самом конце, хотелось бы пригласить всех тех, кто предпочитает узнавать об ошибках не через месяц после коммита от заказчика, а на следующий день от билд-сервера, поделиться своим опытом и высказать замечания к представленном примеру.