RHEL üzerinde Tox ile Python test otomasyonu

Selamlar, bu yazıda RHEL sunucu üzerinde tox'u kurup, birden fazla Python sürümüne karşı testlerimizi nasıl koşturduğumuza bakacağız. Konu basit ama bir-iki yerde insanın canını sıkan ufak detaylar var, ben de tam o detaylara değineceğim. Hadi başlayalım.

Bir Python kütüphanesi yazıyorsanız ya da farklı sunucularda farklı Python sürümleriyle karşılaşacak bir uygulama paketliyorsanız, 'benim makinemde çalışıyordu' cümlesi er ya da geç başınıza patlıyor. Tox tam burada devreye giriyor: izole virtualenv'ler kuruyor, her ortamda paketinizi sıfırdan yüklüyor, testleri çalıştırıyor ve sonucu net bir özetle veriyor. Yani aslında küçük bir CI'yi yerel makineye taşıyorsunuz.

Tox aslında ne yapıyor?

Tox bir tox.ini ya da pyproject.toml dosyası okur. Tanımladığınız her ortam (env) için şu adımları sırayla yapar:

  1. Temiz bir virtualenv açar.
  2. Paketinizi ve tanımladığınız bağımlılıkları kurar.
  3. Belirttiğiniz komutları çalıştırır (genelde pytest).
  4. Geçti / kaldı diye raporlar.

Bu kadar. Sihir yok; ama bu kadarı bile elle yapmaya kalktığınızda kafa şişiriyor.

Birden fazla Python sürümü kurmak

RHEL 9 varsayılan olarak Python 3.9 ile geliyor. Test matrisini büyütmek için AppStream'den ek sürümleri çekiyoruz:

sudo dnf install -y python3 python3-pip python3-devel
sudo dnf install -y python3.11 python3.11-pip python3.11-devel
sudo dnf install -y python3.12 python3.12-pip python3.12-devel

Burada python3.12-devel paketini atlamak ilk anda işe yaramış gibi görünür, ama bağımlılıklarınız içinde C uzantısı derleyen bir şey çıktığında (mesela psycopg2'nin binary olmayan sürümü) hata yer. Bence baştan -devel'i de kurun, dert etmeyin.

Tox'u kurmak

Tox'u sistem geneline değil, kullanıcı seviyesinde kurmak benim tercihim:

pip3 install --user tox
tox --version

Eğer tox komutu bulunamıyorsa ~/.local/bin dizini PATH'inizde olmayabilir. Bunu .bashrc'ye eklemek bir kerelik bir iş.

Ornek bir tox.ini

Kafamızda somutlaşması için küçük bir paket düşünelim. src/mypackage/__init__.py içinde iki fonksiyon olsun:

def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError('Sifira bolme yok')
    return a / b

Test dosyamız tests/test_math.py:

import pytest
from mypackage import add, divide

def test_add():
    assert add(2, 3) == 5

def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(1, 0)

Şimdi tox.ini:

[tox]
envlist = py39, py311, py312, lint
isolated_build = True

[testenv]
deps =
    pytest
    pytest-cov
commands =
    pytest tests/ -v --cov=mypackage --cov-report=term-missing

[testenv:lint]
deps =
    flake8
    black
commands =
    flake8 src/ tests/
    black --check src/ tests/

tox komutunu çalıştırdığınızda dört ortam birden açılır, paketinizi her birine ayrı ayrı kurar ve testleri koşar. tox -e py311 derseniz tek ortam çalışır.

Paralel kosma ve yeniden olusturma

Üç Python sürümü ardı ardına kurulup testler çalışınca süre uzayabiliyor. Paralel modu deneyin:

tox -p auto

Bağımlılık değişip ortamlar bayatladığında ise:

tox -r

Bütün ortamları siler, sıfırdan kurar. CI dışında yerelde sık ihtiyaç duyduğum bir komut, çünkü requirements'a yeni bir paket eklediğinizde tox bunu otomatik fark etmiyor her zaman.

Bizim takildigimiz bir nokta

Şahsi tecrübeden söyleyeyim: bir keresinde RHEL 9 üzerinde py312 ortamı tox listesinde 'görünmüyor' diye uğraşmıştık. Sebebi gayet basitti aslında - paket kurulu değildi ama dnf sessizce başka bir paketi çözmüştü. python3.12 --version denediğimizde 'command not found' aldık. AppStream module setlerinde sürüm bazen gizli kalıyor; dnf module list python ile aktif modülü görmek bu durumda iş kurtarır. Üzerinde bir kahve harcadıktan sonra alışkanlık oldu, ilk iş onu çağırıyorum şimdi.

CI hattina baglamak

GitHub Actions tarafında matris tanımı şu kadar:

name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.9', '3.11', '3.12']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install tox
      - run: tox -e py

tox -e py o anki Python sürümünün ortamını seçiyor; matris bunu üç farklı versiyon için ayrı ayrı tetikliyor. Yerel tox.ini'yi değiştirmeden CI'yi büyütebiliyorsunuz, en hoş tarafı bu.

Sik karsilasilan tuzaklar

  • isolated_build = True'yi unutmak: pyproject.toml ile çalışıyorsanız bu satır olmazsa olmaz. Yoksa tox eski stil setup.py davranışına düşer ve modern build backend'lerinizi tanımaz.
  • deps icindeki surumleri sabitlememek: CI'da yeşil olan tox koşusu ertesi gün kırmızı dönerse, büyük ihtimalle pytest ya da flake8 minor sürüm zıplaması yapmıştır. Kritik araçlar için pytest>=8,<9 gibi bir aralık koyun.
  • passenv'i bos birakmak: Tox güvenlik için ortam değişkenlerinizi temizler. CI'nin koyduğu CI, GITHUB_ACTIONS gibi değişkenler de gider. Lazımsa açıkça passenv = CI HOME GITHUB_* deyin.
  • Sistem Python'u ile karistirmak: pip install tox derken root olmayın. Sistem paketleriyle çakışırsa toparlamak yorucu olur; pip3 install --user tox ya da pipx install tox çok daha temiz.

Kapanis

Bu yazıda tox ile RHEL üzerinde çoklu Python sürümü test ortamını nasıl kurduğumuza, paralel koşmaya ve CI'a bağlamaya kısaca baktık. Bana sorarsanız tox, kütüphane yazan herkes için artık opsiyonel bir araç değil; matris testi olmadan farklı sürümlerde kırılan bir paketi yayınlamanın bedeli her zaman daha ağır oluyor. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.