Tox a jeho použití v CZ.NIC

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.

Autor:

Zanechte komentář

Všechny údaje jsou povinné. E-mail nebude zobrazen.