Построение «правильного» процесса разработки на платформе Mono

    Элементарный пример цикла разработка примитивного 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-скрипта, который имеет следующий вид:
    <?xml version="1.0"?>
        <project name="Example" default="cis" basedir=".">
    
            <!-- Property -->
    
            <!--Targets -->
    
            <target name="cis" description="Execute all targets in CIS.">
                <call target="clean" />
                <call target="build" />
                <call target="documentation" />
                <call target="utest" />
                <call target="gendarme" />
                <call target="stylecop" />
                <call target="sloccount" />
                <call target="cloneanalyze" />
                <call target="ftest" />
            </target>
    </project>
    
    Т.е. сначала объявляются свойства (Property), затем объявляются цели (Target) и в заключении объявляется главная цель (её имя указывается в свойстве default скрипта), которая поочередно вызывает объявленные ранее. Описанный способ не единственный, вместо создания цели вызывающей другие, можно просто прописывать зависимость одних целей от других и тогда вызов целей будет производится автоматически.
    Далее будут реализованы все цели, которые вызывает цель cis. На любом этапе скрипт может быть выполнен, для чего в главной цели (cis) необходимо закомментировать еще нереализованные цели и неиспользуемые свойства.

    Для простоты объявим свойства, которые в дальнейшем сократят нам код.
    Переменная окружения устанавливаемая Jenkins'ом:
            <property name="work.d"      value="${environment::get-variable('WORKSPACE')}" />
    
    Директория в которой собраны инструменты:
            <property name="tools.d"     value="/var/lib/jenkins/tools" />
    
    Алиасы различных директорий, назначение которых очевидно из названия:
            <property name="bin.d"       value="${build.conf}/bin"/>
            <property name="deploy.d"    value="/home/vm/public_html" />
            <property name="test.res.d"  value="test-results" />
            <property name="report.d"    value="${work.d}/reports" />
            <property name="doc.d"       value="${work.d}/doc" />
            <property name="core.d"      value="${work.d}/ExampleCore" />
            <property name="gui.d"       value="${work.d}/ExampleGUI" />
            <property name="utest.d"     value="${work.d}/ExampleUTest" />
            <property name="ftest.d"     value="${work.d}/ExampleFTest" />
    
    Алиасы бинарного файла и файла настроек StyleCopCmd:
            <property name="style.exe"   value="${tools.d}/StyleCop/Net.SF.StyleCopCmd.Console.exe" />
            <property name="style.conf"  value="${tools.d}/StyleCop/Settings.StyleCop" />
    
    Алиасы исполняемого файла, конвертера и файла конфигурации CloneAnalyzer:
            <property name="clone.jar"   value="${tools.d}/CloneAnalyzer/CloneAnalyzer.jar" />
            <property name="clone.conf"  value="${tools.d}/CloneAnalyzer/comments.conf" />
            <property name="clone.conv"  value="${tools.d}/CloneAnalyzer/ca2cpd.exe" />
    
    Алиасы исполняемого файла, обёрточного скрипта, а так же файла с набором тестов и именем хоста на котором развернуто приложение для SeleniumRC:
            <property name="selen.jar"   value="${tools.d}/SeleniumRC/selenium-server-standalone-2.18.0.jar" />
            <property name="selen.sh"    value="${tools.d}/SeleniumRC/selenium.sh" />
            <property name="selen.host"  value="http://192.168.56.210" />
            <property name="selen.suite" value="${ftest.d}/Main.html" />
    

    Приступим к созданию целей.
    1. Первым делом создадим цель по очистке сборочной директории от старых артефактов и созданию необходимых директорий:
            <target name="clean" description="Remove binary files, recreate report directory.">
                <echo message="Target starded at: ${datetime::now()}."/>
                <delete failonerror="false" dir= "${core.d}/${bin.d}"/>
                <delete failonerror="false" file="${core.d}/ExampleCore.pidb"/>
                <delete failonerror="false" dir= "${utest.d}/${bin.d}"/>
                <delete failonerror="false" dir= "${utest.d}/${test.res.d}"/>
                <delete failonerror="false" file="${utest.d}/ExampleUTest.pidb"/>
                <delete failonerror="false" dir= "${gui.d}/${bin.d}"/>
                <delete failonerror="false" dir= "${gui.d}/${test.res.d}"/>
                <delete failonerror="false" file="${gui.d}/ExampleGUI.pidb"/>
                <delete failonerror="false" dir= "${report.d}"/>
                <delete failonerror="false" dir= "${doc.d}"/>
                <delete failonerror="false" file="${work.d}/stylecop.report"/>
                <delete failonerror="false" file="${work.d}/stylecop.violations.xml"/>
                <mkdir dir="${report.d}"/>
                <mkdir dir="${report.d}/gendarme"/>
                <mkdir dir="${report.d}/sloccount"/>
                <mkdir dir="${report.d}/cloneanalyzer"/>
                <mkdir dir="${report.d}/selenium"/>
                <mkdir dir="${doc.d}/xml"/>
                <mkdir dir="${doc.d}/html"/>
                <echo message="Target completed at: ${datetime::now()}."/>
            </target>
    

    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
    <IfModule mod_alias.c>
        Alias /repos "/srv/svn/html"
    </IfModule>
    <Directory /srv/svn/html>
        Options        +Indexes +Multiviews -FollowSymLinks
        IndexOptions   FancyIndexing \
                       ScanHTMLTitles \
                       NameWidth=* \
                       DescriptionWidth=* \
                       SuppressLastModified \
                       SuppressSize
        order allow,deny
        allow from all
    </Directory>
    
    <Location /repos/Example>
        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
    </Location> 
    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 << EOF
    Example_commiters: Admin User
    Example_readers: Admin User CIS
    
    touch /srv/svn/user_access/accessfile
    cat > /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:
            <target name="build" description="Compiles the source code.">
                <echo message="Target starded at: ${datetime::now()}."/>
                <echo message="Building ExampleCore."/>
                <csc codepage="utf8" target="library"
                    output="${core.d}/${bin.d}/${build.conf}/ExampleCore.dll"
                    doc="${doc.d}/xml/ExampleCore.xml">
                    <sources>
                        <include name="${core.d}/**.cs"/>
                    </sources>
                </csc>
                <copy
                    file="${core.d}/${bin.d}/${build.conf}/ExampleCore.dll"
                    tofile="${gui.d}/${bin.d}/${build.conf}/ExampleCore.dll" />
                <copy
                    file="${core.d}/${bin.d}/${build.conf}/ExampleCore.dll"
                    tofile="${utest.d}/${bin.d}/${build.conf}/ExampleCore.dll" />
    
                <echo message="Building ExampleGUI."/>
                <csc codepage="utf8" target="library"
                    output="${gui.d}/${bin.d}/${build.conf}/ExampleGUI.dll"
                    doc="${doc.d}/xml/ExampleGUI.xml">
                    <sources>
                        <include name="${gui.d}/**.cs" />
                    </sources>
                    <references>
                        <include name="System.Web.dll" />
                        <include name="${gui.d}/${bin.d}/${build.conf}/ExampleCore.dll" />
                    </references>
                </csc>
                <echo message="Target completed at: ${datetime::now()}." />
            </target>
    

    Настроим плагин Warnings таким образом, что бы сообщения компилятора обрабатывались Jenkins'ом:

    В дальнейшем это позволит получать вот такие отчеты по Warning'ам компилятора:

    4. Сгенерируем документацию в формате html:
            <target name="documentation" description="Generation documentation.">
                <echo message="Target starded at: ${datetime::now()}."/>
                <exec
                    program="monodocer"
                    commandline="
                        -pretty
                        -i:${doc.d}/xml/ExampleCore.xml
                        -assembly:${core.d}/${bin.d}/${build.conf}/ExampleCore.dll
                        -path:${doc.d}/xml"/>
                <exec
                    program="monodocer"
                    commandline="
                        -pretty
                        -i:${doc.d}/xml/ExampleGUI.xml
                        -assembly:${gui.d}/${bin.d}/${build.conf}/ExampleGUI.dll
                        -path:${doc.d}/xml"/>
                <exec
                    program="mdoc"
                    commandline="export-html ${doc.d}/xml -o=${doc.d}/html"/>
                <echo message="Target completed at: ${datetime::now()}." />
            </target>
    

    Настроим плагин DocLinks:В результате на главной странице проекта будет создана ссылка на документацию.

    5. Соберём и запустим юнит-тесты:
            <target name="utest" description="Test the source code.">
                <echo message="Target starded at: ${datetime::now()}."/>
                <echo message="Building ExampleUTest."/>
                <csc codepage="utf8" target="library"
                    output="${utest.d}/${bin.d}/${build.conf}/ExampleUTest.dll">
                    <sources>
                        <include name="${utest.d}/**.cs" />
                    </sources>
                    <references>
                        <include name="System.Web.dll" />
                        <include name="${gui.d}/${bin.d}/${build.conf}/ExampleCore.dll" />
                        <include name="nunit.core.dll" />
                        <include name="nunit.framework.dll" />
                    </references>
                </csc>
    
                <echo message="Launch NUnit." />
                <nunit2 haltonfailure="false">
                    <formatter type="Xml"
                        usefile="true"
                        extension=".xml"
                        outputdir="${utest.d}/${test.res.d}" />
                    <formatter type="Plain" usefile="false" />
                    <test assemblyname="${utest.d}/${bin.d}/${build.conf}/ExampleUTest.dll" />
                </nunit2>
                <echo message="Target completed at: ${datetime::now()}." />
            </target>
    

    Настроим плагин NUnit:

    Отчеты плагина NUnit выглядят следующим образом:

    6. Выполним статический анализ кода при помощи Gendarme:
            <target name="gendarme">
                <echo message="Target starded at: ${datetime::now()}."/>
                <echo message="Check code by Gendarme."/>
                <exec program="gendarme" failonerror="false"
                    commandline="
                        --config GendarmeRules.xml ${core.d}/${bin.d}/${build.conf}/ExampleCore.dll
                        --xml ${report.d}/gendarme/ExampleCore.gendarme.xml
                        --severity medium+
                        --confidence total"/>
                <exec program="gendarme" failonerror="false"
                    commandline="
                        --config GendarmeRules.xml ${gui.d}/${bin.d}/${build.conf}/ExampleGUI.dll
                        --xml ${report.d}/gendarme/ExampleGUI.gendarme.xml
                        --severity medium+
                        --confidence total"/>
                <echo message="Target completed at: ${datetime::now()}." />
            </target>
    
    … и StyleCopCmd:
            <target name="stylecop">
                <echo message="Target starded at: ${datetime::now()}."/>
                <exec program="mono"
                    commandline="
                        ${style.exe}
                        -r
                        -sc ${style.conf}
                        -d ${work.d}
                        -of ${work.d}/stylecop/stylecop.report"/>
                <echo message="Target completed at: ${datetime::now()}." />
            </target>
    
    Что бы избавиться ошибки, вызываемой 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 <filename unknown>:0 
     at System.IO.FileStream..ctor (System.String path, FileMode mode, FileAccess access, FileShare share) [0x00000] in <filename unknown>: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 <filename unknown>:0 
     at System.IO.StreamWriter..ctor (System.String path, Boolean append, System.Text.Encoding encoding) [0x00000] in <filename unknown>: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 <filename unknown>:0
    
    Создадим вожделенный файл с правами на запись всем желающим:
    sudo touch /etc/mono/registry/last-btime
    sudo chmod 666 /etc/mono/registry/last-btime
    

    Настроим плагин Violations:

    Так будут выглядеть отчеты:

    Тут есть несколько неприятных моментов: во-первых: для того, что бы успешно отображался детализированный отчет по StyleCop отчет должен лежать в корне проекта (что нарушает общую тенденцию), а во-вторых: детализированный отчет по Gendarme мне так и не удалось настроить (кто сталкивался — прошу помощи).

    7. Собираем метрики:
            <target name="sloccount">
                <echo message="Target starded at: ${datetime::now()}."/>
                <echo message="Analyze code by SLOCCount."/>
                <exec program="sloccount"
                    output="${report.d}/sloccount/sloccount.report"
                    commandline="
                        --duplicates
                        --wide
                        --details ${work.d}"/>
                <echo message="Target completed at: ${datetime::now()}." />
            </target>
    

    Настроим плагин SLOCCount:

    Пример отчета:

    8. Выполняем поиск дубликатов в два этапа: сначала запустим приложение CloneAnalyzer, а потом выполним конвертацию полученного отчета:
            <target name="cloneanalyze">
                <echo message="Target starded at: ${datetime::now()}."/>
                <echo message="Find code duplicates by CloneAnalyze."/>
                <exec program="java" failonerror="false"
                    output="${report.d}/cloneanalyzer/cloneanalyzer.report.txt"
                    commandline="
                        -jar ${clone.jar}
                        -c ${clone.conf}
                        -f .*\.\(cs\|aspx\)
                        -d ${work.d}"/>
                <echo message="Convert CloneAnalyze report in CPD report."/>
                <exec program="mono" failonerror="false"
                    commandline="
                        ${clone.conv}
                        ${report.d}/cloneanalyzer/cloneanalyzer.report.txt
                        ${report.d}/cloneanalyzer/cloneanalyzer.report.xml"/>
                <echo message="Target completed at: ${datetime::now()}." />
            </target>
    

    Настроим плагин DRY:

    Пример отчета:

    9. Выполним функциональные тесты, для чего сначала развернем приложение, перезапустим web-сервер и собственно запустим тесты:
            <target name="ftest">
                <echo message="Target starded at: ${datetime::now()}."/>
                <echo message="Prepare to launch tests."/>
                <copy todir="${deploy.d}/${bin.d}/">
                    <fileset basedir="${gui.d}/${bin.d}/${build.conf}">
                        <include name="*.dll"/>
                    </fileset>
                </copy>
                <copy todir="${deploy.d}/">
                    <fileset basedir="${gui.d}/">
                        <include name="*.aspx"/>
                        <include name="*.asax"/>
                        <include name="*.config"/>
                    </fileset>
                </copy>
    
                <echo message="Restart apache2 server."/>
                <exec program="/bin/bash" commandline="-c 'sudo /etc/init.d/apache2 restart'"/>
    
                <echo message="Testing project by Selenium."/>
                <exec program="${selen.sh}" failonerror="false"
                    commandline="
                        -htmlSuite
                        *firefox
                        ${selen.host}
                        ${selen.suite}
                        ${report.d}/selenium/selenium.html"/>
                <echo message="Target completed at: ${datetime::now()}." />
            </target>
    

    Для того, что бы пользователь jenkins мог выполнять перезапуск web-сервера, ему необходимо дать соответствующие права:
    su
    cat > /etc/sudoers << EOF
    jenkins  ALL=(ALL) NOPASSWD: /etc/init.d/apache2
    EOF
    exit
    

    Для запуска тестов создадим скрип-обёртку, который будет устанавливать необходимые переменные окружения и транслировать через себя параметры командной строки Selenium-серверу:
    cd /var/lib/jenkins/tools/SeleniumRC
    touch selenium.sh
    chmod +x selenium.sh
    cat > 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 и укажем расположение приложения:
    <IfModule !mod_mono.c>
        LoadModule mono_module /usr/lib/apache2/mod_mono.so
    </IfModule>
    
    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"
    <Location "/">
        Allow from all
        Order allow,deny
        MonoSetServerAlias Example
        SetHandler mono
        SetOutputFilter DEFLATE
        SetEnvIfNoCase Request_URI "\.(?:gif|jpe?g|png)$" no-gzip dont-vary
    </Location>
    
    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.

    Ну и в самом конце, хотелось бы пригласить всех тех, кто предпочитает узнавать об ошибках не через месяц после коммита от заказчика, а на следующий день от билд-сервера, поделиться своим опытом и высказать замечания к представленном примеру.
    • +15
    • 11,6k
    • 3
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 3
    • 0
      Все то же самое, но гораздо проще по настройке, можно сделать с помощью TeamCity
      • +1
        Все круто, но! Забыли очень важный аспект, а именно деплоймент.

        Во-первых, нет артефакта в виде бинарей сайта (см. Copy Artifact Plugin). Во-вторых, нет педали, которая этот артефакт угонит на тестовый\боевой стенд и там все развернет.

        Поставьте себя на место деливери/QA — как бы вы разворачивали сайт, не имея под рукой ни xbuild, ни кода?
        • 0
          > Все круто, но! Забыли очень важный аспект, а именно деплоймент.
          Спасибо за замечание!
          > Во-первых, нет артефакта в виде бинарей сайта (см. Copy Artifact Plugin).
          Эмм… Не совсем понял.
          >Во-вторых, нет педали, которая этот артефакт угонит на тестовый\боевой стенд и там все развернет.
          Согласен. В данном примере разворачивание (если это можно так назвать) производится на локальной машине, простым копированием бинарных файлов, и страниц в отдельную папку и перезапуском Apache. Производится это в рамках цели функционального тестирования (Selenium) и сделано для того, что бы обозначить, что данный шаг присутствует.
          Пожалуй это не правильно и будет лучше перенести deploy в отдельную цель.
          > Поставьте себя на место деливери/QA — как бы вы разворачивали сайт, не имея под рукой ни xbuild, ни кода?
          Задумка такова, что в рамках Jenkins-проекта при успешной компиляции приложение автоматически будет развернуто на сервере (серверах) заданных в конфигурации. И получается, что для тестировщиков разворачивание будет происходить автоматически (хотя возможно это и не правильно, т.к. как откатиться к любой из предыдущих версий?). На некоторые вопросы мне сложно рассуждать, из-за отсутствия соответствующего опыта.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое