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.