Před nějakou dobou jsme se v CZ.NIC rozhodli k testování našich projektů v Pythonu použít projekt Tox. Tento projekt umožňuje sjednotit testování projektů v různých prostředích (lokálních vývojových i těch na integračních serverech) a také usnadňuje testování možných kombinací závislostí. Obzvláště druhý bod se nám s přechodem na Python 3 zdál důležitý.
Postupným vývojem a překonáváním drobných (i větších) překážek jsme nakonec dospěli ke stabilní Tox konfiguraci, která splňuje všechny naše požadavky. Níže se pokusím jednotlivé vlastnosti této konfigurace trochu osvětlit a rozebrat.
Konfigurace projektu Tox
Naše finální konfigurace pro projekt „rdap“ vypadá následovně:
[tox]
minversion = 3.0.0
envlist = quality,clear-coverage,{py27,py35}-django{10,11},compute-coverage
[testenv]
setenv =
PYTHONPATH = {toxinidir}/test_cfg:{env:IDL_DIR:}
DJANGO_SETTINGS_MODULE = settings
passenv =
IDL_DIR
PYTHONWARNINGS
debian_deps =
py27: python-omniorb
py35: python3-omniorb
skip_install =
coverage: True
install_command = pip install --process-dependency-links {opts} {packages}
extras = testing
deps =
coverage
!thaw: -cconstraints.txt
django10: Django>=1.10,<1.10.99
django10: pytz
django11: Django>=1.11,<1.11.99
commands =
coverage run --parallel-mode --source=rdap --branch -m django test rdap
[testenv:clear-coverage]
commands =
coverage erase
[testenv:compute-coverage]
commands =
coverage combine
coverage report --include=*/tests/* --fail-under=100
coverage report –omit=*/tests/*
[testenv:py27-thaw]
[testenv:py35-thaw]
[testenv:quality]
extras = quality
# Do not fail on first error, but run all the checks
ignore_errors = True
deps =
commands =
isort --recursive --check-only --diff rdap
flake8 --format=pylint --show-source rdap
pydocstyle rdap
První částí je základní hlavička konfiguračního souboru, která specifikuje minimální požadovanou verzi Tox (kvůli použití negativních faktorů) a také seznam prostředí, která se mají spustit při použití příkazu „tox“. Ta jsou specifikována pomocí tzv. faktorů (výčet uvedený ve složených závorkách, jednotlivé faktory jsou odděleny pomlčkou) a při provádění pak dochází k expanzi do matice. Výsledný seznam prostředí (a jejich pořadí) je tedy následující:
quality clear-coverage py27-django10 py27-django11 py35-django10 py35-django11 compute-coverage
Následuje blok specifikující základní nastavení testovacího prostředí a také definuje, jaké příkazy se budou provádět. Blok „debian_deps“ je konfigurací pro„tox-debian-plugin“, který se stará o instalaci Debianích balíků přímo do daného virtuálního prostředí. Tento blok také využívá faktorů a specifikuje různé závislosti v návaznosti na verzi Pythonu. Pomocí volby „skip_install“ specifikujeme, že pro prostředí, které obsahují faktor „coverage“ není nutné balík instalovat. Dále potřebujeme definovat vlastní instalační příkaz (přidána volba „–process-dependency-links“ – je důležité pro implementaci hooku) a také definujeme, jaké extra komponenty testovaného projektu se mají instalovat a jaké další balíky (kromě tech uvededených v setup.py) je nutné doinstalovat. Zde je opět použito faktorů a to i negativních („!thaw“ značí všechna prostředí, která neobsahují faktor „thaw“). Volbou „commands“ jsou specifikovány příkazy, které se mají provést v rámci běhu daného virtuálního prostředí.
Nastavení uvedená v bloku „[testenv]“ jsou brána jako základní a je možno je přetěžovat či k nim přidávat. Toho se využívá při definici prostředí pro zpracování výsledků coverage, kde se sice používá stejné natavení jako v základním bloku, ale jsou zde změněny příkazy, které se provádějí.
Dále je také možné specifikovat jiná prostředí, která nejsou uvedená v definici „envlist“. Takto definovaná prostředí je možno spustit ručně pomocí přepínače „-e“. Takto máme definována prostředí pro testování aktuálních verzí závislostí. Posledním použitým prostředím je statická kontrola kvality, kde je vynulován seznam závislostí kvůli urychlení vytváření prostředí a také jsou tu ignorovány výsledky jednotlivých příkazů. To donutí Tox dokončit všechny příkazy uvedené v bloku „commands“ a nahlásit až souhrnný výsledek.
Správa závislostí na interních projektech
V našem vývojovém cyklu a prostředí našich aplikací dochází často k tomu, že vývojová větev závisí na ješte nezačleněných větvích v jiných interních projektech. Tento stav pro Tox (i pro nás) představuje problém, protože bychom museli v „setup.py“ vždy uvádět konkrétní vývojovou větev závislých projektů. Tento způsob je ale zcela nevhodný, protože by bylo nutné setup.py při integraci do hlavní větve měnit a mohlo by docházet k chybám.
Rozhodli jsme se tedy využít možností psaní pluginů pro Tox a napsat si plugin, který se o podobnou věc postará za nás.
Nejprve jsme vytvořili repozitář pro správu závislostí mezi projekty. Z formátů nakonec vyhrál JSON díky své jednoduché struktuře a přítomnosti parseru přímo ve standardní knihovně. Náš konfigurační soubor má následující strukturu:
{
„project1“: {
„origin“: „project_1_url“,
„revision“: „an_awesome_feature“
},
„project2“: {
„origin“: „projectu_2_url“,
„revision“: „yet_another_feature“
}
}
Tento konfigurační soubor se jmenuje „our_feature.conf“ a jednoduše znamená, že aby někomu fungovala naše vývojová větev pojmenovaná „our_feature“, musí si v projektech 1 a 2 přepnout na větve „an_awesome_feature“, resp. „yet_another_feature“.
Toto nastavení jednotlivých větví je tedy třeba provést i při automatickém testování v rámci continuous integration. K nám tomu slouží implementace Tox pluginu. Ten se pomocí „pluggy.hookimpl“ zapojí do procesu instalace závislostí a provede následující kroky:
1) Naklonuje výše uvedený repozitář s konfiguracemi prostředí do dočasného adresáře,
2) zkontroluje, zda se tam nachází konfigurační soubor pro aktuální větev,
3) zmodifikuje soubor instalovaného projektu obsahující URL repozitáře pro závislé projekty a přidá k nim specifikace větve (volba „dependency_links“ v setup.py),
4) spustí znovu instalaci závislostí daného projektu.
Tento postup není ideální, protože se dané prostředí vytváří dvakrát, ale v současné době pro nás asi neexistuje lepší řešení. Naše interní projekty nejsou umístěny v PyPi a jsou tedy instalovány přímo přes URL repozitářes pomocí přepínače „–process-dependency-links“.
Celá implementace hooku pak vypadá následovně:
@hookimpl
def tox_testenv_install_deps(venv, action):
"""Parse dependency links and update dependencies."""
if not os.path.isfile(venv.envconfig.dep_links):
return None
action.setactivity('PR-VERSION', 'parse version depencencies')
tmp_dir = mkdtemp(prefix='pr-version-')
try:
action.setactivity('PR-VERSION', 'git clone')
action.popen(['git', 'clone', 'git@gitlab.office.nic.cz:pr-utils/pr-version.git', '--depth', '1', tmp_dir])
action.setactivity('PR-VERSION', 'parse dependency links {}'.format(venv.envconfig.dep_links))
# Make a copy of dep_links
copy(venv.envconfig.dep_links, tmp_dir)
update = parse_deps(venv, venv.envconfig.dep_links, action, tmp_dir)
if update:
action.setactivity('PR-VERSION', 'dependencies changed - running sdist again')
# This will recreate the package according to the settings and tox params
venv.session.get_installpkg_path()
# Replace the modified dep_links from backup
copy(os.path.join(tmp_dir, venv.envconfig.dep_links), venv.envconfig.dep_links)
finally:
rmtree(tmp_dir)
Veškerá práce se zjišťováním aktuální verze a přepisováním konfiguračního souboru je bohužel natolik specifická, že je zbytečné ji zde uvádět. Závisí dost značně nejen na struktuře projektu jako takového, ale i na tagovacích a releasovacích zvyklostech projektů.
Shrnutí
Celkově jsme s použitím projektu Tox pro sjednocení vývojových prostředí velmi spokojeni a postupně na testování pomocí Tox přecházíme se všemi projekty. Za hlavní výhody považujeme jednoduchou rozšiřitelnost na další prostředí, reprodukovatelnost prostředí a komplexní správu sestavovací matice.