Skip to content

Instantly share code, notes, and snippets.

@dpwiz
Created February 28, 2011 19:02
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save dpwiz/847813 to your computer and use it in GitHub Desktop.
Save dpwiz/847813 to your computer and use it in GitHub Desktop.
Prepare to deploy stuff
mkdir -p reports
REPO="http://127.0.0.1:8888"
echo Checking for local repo at ${REPO}
curl -f --head ${REPO} > /dev/null 2>&1 || REPO='http://pypi.python.org/simple'
echo "... will use ${REPO}"
echo 'Creating "venv" environment...'
virtualenv --distribute --no-site-packages venv
echo 'Installing dependencies to "venv" environment...'
if [ x$1 == x"--ci" ]; then
echo "Testing mode."
PIP=reqs
else
echo "Dev/deploy mode."
PIP=stuff
fi
./venv/bin/pip install -q -i ${REPO} -E venv -r ./pip-${PIP}.txt
virtualenv --relocatable venv
nose
WebTest
coverage
pylint
Программист зверь ленивый, поэтому всё, что будет делаться больше одного раза надо непременно заскриптовать.
Я уже некоторое время ковыряю <a href="http://welinux.ru/tag/tdd">TDD</a> и задача постоянного контроля качества для меня становится всё актуальней. Особенно при пополнении команды новыми разработчиками.
Сначала я запускал тесты руками: save, switch, $ nosetests. Потом к тестам добавились проверялки качества кода и пришлось всё засунуть в скрипт:
<code lang="bash">pyflakes *.py
pep8 *.py
pylint *.py
nosetests</code>
Скрипт запускать каждый раз ужасно лениво, поэтому небольшая оболочка на inotifywait стала запускать тесты и проверки после каждого сохранения:
<code lang="bash">while true; do
inotifywait -e modify project/*.py -qq; clear
./do_tests
done</code>
Тут я стал более-менее доволен происходящим и даже на некоторое время расслабился. Но ведь программист кроме того, что ленив ещё и горд, поэтому результаты хочется кому-нибудь показать. Чтобы вести историю происходящего (которая очень помогает когда заходит начальник начальника и спрашивает: «ну-с, чем вы занимались последний месяц?») уже есть система контроля версий. Но она показывает только, что сделано и не даёт обзора успешности каждой ревизии. Получается что код лежит, но непонятно в каком он состоянии и что где ещё надо сделать.
Кроме того довольно тяжело следить за коллегами, которые тоже могут что-то сделать и забыть прогнать тесты, в результате в репозитории лежит битый код, не прошедший code review и при очередном pull может внезапно начаться clusterfuck.
И тут очень вовремя kmmbvnr@lj выпустил <a href="http://narod.ru/disk/6361429001/out.ogv.html">скринкаст</a>, в котором он демонстрировал интеграцию тестирования для django-проектов с сабжем Jenkins (бывш. Hudson). Посмотрел я на все эти красоты, графики и отчёты и тоже захотел чтобы всё само пело и играло. Но у него django-jenkins, как и следует из названия, встраивается в джангу и генерит отчёты используя хитрую систему. Мой проект до джанги не дорос и скорее всего не дорастёт — это достаточно тривиальное WSGI-приложение, которое правда стремительно разрастается. Пришлось поднимать всё с нуля.
Воскресенье я на это убил, но в целом всё довольно прямолинейно и теперь у меня есть симпатичные отчёты:
<a href="http://imgur.com/LinZT" title="Hosted by imgur.com"><img src="http://i.imgur.com/LinZTs.jpg" alt="" title="Hosted by imgur.com" /></a>
<b>Что внутри?</b>
[cut]
0) Автоматическое получение свежих версий из репы. Система очень плотно интегрирована с меркуриалом, поэтому все правки и сообщения о коммитах там видны и доступны.
1) Сборка проекта с нуля, как если бы это выкатывалось на боевой сервер. Создаётся окружение, выкачиваются и ставятся модули и всё такое. Сама система находится на сервере разработчиков в условиях приближеных к боевым: фря, изоляция питонских проектов, гемор с установкой.
2) Запускаются тесты. Вместе с ними сразу происходит проверка покрытия тестами кода. В результате создаются файлы отчётов о заваленых тестах и покрытии. Зелёный график сверху - количество заваленых тестов показывается там же красным. Самый нижний график показывает покрытие. Покрытие можно посмотреть прям в файлах с подсветкой непровереных строк.
3) Выполняется проверка качества: pep8 и pylint тщательно давят на мозги разработчику добиваясь от него порядка в коде, именах переменных и прочего. Красная ломаная, которая постоянно будет какбы намекать, что пора бы уже вынести мусор.
4) Разработчик, закоммитивший битый код будет автоматически пожурён системой в email и jabber (лично и в MUC), а потом и ведущим програмистом, который тоже получит аналогичную жалобу. Потому что никто не ожидает испанской инквизиции!
В итоге у нас есть система, которая берёт на себя значительную часть рутины сразу с нескольких человек.
<b>Дайте две!</b>
Сама система поставляется в виде <a href="http://mirrors.jenkins-ci.org/war/latest/">java/war пакета</a>. Ничего устанавливать не надо, но понадобится JRE (под фрю я поставил Diablo-1.6). На <a href="http://jenkins-ci.org/">сайте</a> можно ткнуть ссылку и сразу запустить её у себя. Запускается просто: java -jar jenkins.war. Там ещё есть опции для указания портов и всего такого. Рекомендую биндить на локалхост и завернуть в nginx. Мало ли что. От рута запускать категорически не советую.
На фре оно у меня отказалось демонизироваться, поэтому я завернул в <a href="http://supervisord.org/">supervisor</a>. Потом он ещё пригодился. Устанавливается просто - pip install supervisord.
Все настройки производятся уже из браузера после запуска в меню Manage Jenkins.
Первым делом надо воткнуть плагинчиков. Они качаются и ставятся сами, надо только галочки расставить.
<ul><li>Jenkins Cobertura Plugin - отчёты о test coverage</li>
<li>Hudson instant-messaging plugin - уведомления. Нужно для jabber. Ну или не нужено, если вы это не будете ставить.</li>
<li>Hudson Jabber notifier plugin - собственно жабер.</li>
<li>Hudson Mercurial plugin - интеграция с репозиторием.</li>
<li>Hudson Violations plugin - обработчик pylint</li>
</ul>
Остальные уже установленые я отключил, ибо нефиг.
Некоторе плагины без рестарта не появятся в настройках.
<b>Настройки</b>
Сразу стоит сделать enable security, и выбрать подходящий способ авторизации. Я взял «Project-based Matrix Authorization Strategy». Юзера создавать не надо, а вот рестартнуться надо. При повторном заходе будет предложеносразу сделать админа. Лучше его так и назвать - admin. Иначе потом можно запутаться кто есть кто, потому что оно ведёт список участников проекта глядя на отметившихся в коммитах.
«Prevent Cross Site Request Forgery exploits» штука хорошая, но у меня с ней не заработало, пришлось не включать.
JDK и другие «installations» настраивать не надо, всё и так работает.
Shell executable стоит выставить в /usr/local/bash или /usr/bin/bash. Короче в полный путь к шелу, а то мало ли чем оно там будет запускать... При большом желании можно хоть питон назначить, но это неудобно.
Jabber и Email секции вам поможет настроить Кэп и кнопка Advanced.
<b>Настройка проекта</b>
Которые тут называются Job. Указываем job name и отметку «free-style». Дальше самое вкусное, на что я и убил половину выходных.
Билдов у меня много и делаю я их часто, поэтому показалось логичным установить лимит хранения в 10 штук / 30 дней. Если что - всегда можно нажать Build Now.
Код я выкладываю на приватную репу в <a href="http://bitbucket.org/">bitbucket</a> потому что они бесплатные и довольно фичастые при этом. Git я нелюблю т.к. питонист на всю голову и у гитхаба нет бесплатных приватных реп.
Repository URL будет такой (да, в нём надо передавать авторизацию. именно поэтому свою инсталяцию jenkins надо сразу запаролить): [code]https://юзернейм:пароль@bitbucket.org/юзернейм/uniproxy[/code]
Repository Browser - bitbucket.
Build triggers - poll SCM. Schedule в формате крона: [code]*/5 * * * *[/code]
Наверно можно было бы ещё сделать trigger remotely, но мне лень. Это ж надо каждому девелоперу потом будет конфиги с этим триггером таскать... А корзинка всегда на одном месте и отовсюду доступна.
<b>Шаги <span style="text-decoration:line-through;">инквизиции</span> экзекуции</b>
Файлы проекта организованы следующим образом:
<code lang="bash">.
buildenv.sh
pip-reqs.txt
pylint.rc
project/*.py
reports/*
venv/*</code>
Последние два создаются скриптом. Основные я выложил <a href="https://gist.github.com/847813">на гитхабе</a>.
В скринкасте я видел, что всё в одном шаге, но решил всё же разбить на три логических: build, check, test.
Первый совсем простой: «./buildenv.sh» — тот самый, что лежит в проекте и готовит virtualenv. Можно было бы скопипастить его сюда, но это не <span style="text-decoration:line-through;">труъ</span> DRYъ т.к. пришлось бы держать дубликаты одного и того же.
Второй посложнее:<code lang="bash">#!/usr/local/bin/bash
venv/bin/pep8 --repeat --ignore=E501,W391 project | perl -ple 's/: ([WE]\d+)/: [$1]/' > reports/pylint.report
venv/bin/pylint --rcfile pylint.rc project/*.py >> reports/pylint.report
echo "pylint complete"</code>
Первая и последняя строки связаны с тем, что если pylint найдёт к чему придраться, то весь шаг будет считаться заваленым. А pylint хуже самой злобной училки, ВСЕГДА найдёт к чему придраться. В конфиге к нему прописал, что некоторые ошибки мне неинтересны. pep8 дополняет pylint, но больше внимания уделяет оформлению. Некоторые предупреждения отключены (длину строк проверяет pylint и там она задана в конфиге).
Обратите внимание, что все скрипты запускаются из venv, в который были установлены на предыдущем шаге. Если там что-то развалится, всё остальное тоже порушится и на доске <span style="text-decoration:line-through;">позора</span> будет висеть большой красный шар.
Третий запускает тесты и собирает покрытие:
<code lang="bash">venv/bin/coverage run --include 'project/*.py' project/tests.py --with-xunit --xunit-file=reports/tests.xml --where=project
venv/bin/coverage xml -o reports/coverage.xml</code>
Тут надо быть очень осторожным с путями. Если они будут не от корня репозитория, а от project, то отображение построчного покрытия не будет работать т.к. не сможет найти исходники. После вдумчивого курения опций, гугления и коментирования я всё же подобрал рабочий вариант.
<b>Отчёты</b>
Метрики собраны, теперь их надо загрузить и показать. Оно там может ещё много чего полезного делать, но мы сейчас просто абьюзим её на предмет красивых графиков. До первой сборки оно будет говорить, что файлов отчётов нет таких, но это не страшно.
Включаем «Publish JUnit test result report» и, хотя они никакие не JUnit, а nosetests указываем **/reports/tests.xml, что значит «от корня репы/задания в каталоге reports.
Следующим пунктом идёт «Report Violations» и там pylint. Несмотря на то, что в названии поля стоит «XML filename pattern», а pylint выдаёт никакой не xml, так же указываем файл **/reports/pylint.report где они на пару с pep8 всячески критикуют код неряшливых разработчиков с занесением в личное дело. Это очень помогает утром включиться в работу: пришёл, глянул график violations и пока исправлял, хоть вспомнил что вчера писал.
Ну и последнее. «Publish Cobertura Coverage Report» лежащий в «**/reports/coverage.xml». Я понятия не имею что такое Cobertura, но формат питонского coverage оно понимает исправно.
У pylint и coverage можно выставить границы «погоды», которые будут влиять на общее состояние проекта. Дефолтные значния вроде подходят.
<b>Кодер птица гордая — не пнёшь, не полетит</b>
В самом конце идут уведомления по email и jabber, можно настроить по вкусу. Единственное, что их надо сначала настроить в основном конфиге сервера интеграции, а то оно работать не будет.
<b>Поехали и махнул рукой</b>
Настройки записаны, можно отправить на сборку. Либо вручную через кнопку Build Now, либо закоммитив и запушив что-нибудь в репу. Внизу сайдбара, в Build History появится новая задача и бегунок прогресса, кликнув на который можно перейти в раздел «Console Output» текущего билда и смотреть за ним в прямом эфире. Когда всё будет окончено, в Status появится всякая всячина по которой можно полазить и посмотреть как что где.
<b>Bonus!</b>
Каждый раз воссоздавая venv оно качает все модули с pypi и делает это очень долго. Кроме того это трафик. Немного поискав я нашёл в pypi <a href="http://pypi.python.org/pypi/collective.eggproxy">collective.eggproxy</a>, кэширующий прокси-репозиторий мимикрирующий под pypi.python.org/simple. Запускается просто как `eggproxy_run`. У него нет справки и по-умолчанию он складывает всё в /var/www, что не есть хорошо. Почитав его доки на сайте можно узнать как сделать файл конфига, чтобы настроить пути и порты. Оно тоже не захотело демонизироваться, поэтому вслед за Jenkins было отправлено в объятия supervisord.
buildenv.sh уже натренирован адаптироваться к наличию/отстутствию этой прокси, там всё просто.
<b>Credits</b>
<a href="http://kmmbvnr.livejournal.com/75183.html">kmmbvnr@lj: Как начать тестировать и получать от этого удовольствие</a>. Кроме интеграции с Jenkins описывается ещё <a href="https://github.com/kmmbvnr/django-any">django-any</a>. Всем джангистам настоятельно рекомендую ознакомиться и пользоваться.
<a href="http://www.rhonabwy.com/wp/2009/11/04/setting-up-a-python-ci-server-with-hudson/">Setting up a python CI server with Hudson</a>, всё ещё актуальный пост с правильными советами, на основе которого сделана эта моя интерпретация.
Ещё для тестирования проекта была задействована <a href="http://pythonpaste.org/webtest/">простая обёртка над WSGI приложениями</a>, позволяющая без лишнего гемороя заниматься тестированием и отладкой без wsgi-контейнеров и ручной работы в браузере.
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
[MESSAGES CONTROL]
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifier separated by comma (,) or put this option
# multiple time.
disable=F0401,R0201
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=parseable
# Include message's id in output
include-ids=yes
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (R0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (R0004).
comment=no
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching names used for dummy variables (i.e. not used).
dummy-variables-rgx=_|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed.
generated-members=REQUEST,acl_users,aq_parent
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,apply,input
# Regular expression which should only match correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Regular expression which should only match correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Good variable names which should always be accepted, separated by a comma
good-names=app,uwsgi,e,i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=__.*__|[Tt]est.*
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branchs=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=0
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
while true; do
inotifywait -e modify project/*.py -qq; clear
pyflakes project/*.py && nosetests --where project
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment