Linting symbolic code Python/Rust di CI perlu diperlakukan sedikit berbeda dari linting aplikasi biasa. Setelah tren komputasi simbolik kembali menguat, termasuk dengan munculnya Symbolica 2.0 sebagai konteks ekosistem, tim sering mulai menambahkan modul simbolik ke codebase Python atau Rust yang sebelumnya hanya berisi kode numerik atau backend umum.
Masalahnya, build yang awalnya stabil bisa mulai gagal karena hasil yang tidak deterministik, perbedaan versi tool lokal vs CI, serialisasi ekspresi yang berubah, atau test yang sulit direproduksi. Solusinya bukan sekadar menambah linter, tetapi membuat quality gate yang memeriksa format, lint, tipe, test smoke, dan determinisme hasil dengan aturan yang jelas serta biaya eksekusi yang tetap terkendali.
Mengapa kode simbolik butuh quality gate yang lebih ketat
Pada kode simbolik, bug tidak selalu muncul sebagai crash. Sering kali problem muncul sebagai:
- ekspresi yang setara secara matematis tetapi terserialisasi berbeda,
- urutan suku yang berubah antar mesin atau antar versi dependency,
- hasil penyederhanaan yang valid tetapi tidak stabil untuk snapshot test,
- perbedaan perilaku antara binding Python dan implementasi Rust,
- test yang lolos lokal tetapi gagal di CI karena fitur opsional atau backend native berbeda.
Karena itu, quality gate untuk repositori symbolic code sebaiknya memisahkan beberapa lapisan:
- style gate: format dan lint dasar,
- static gate: type checking dan kompilasi,
- semantic gate: smoke test ekspresi simbolik dan validasi determinisme,
- integration gate: memastikan Python dan Rust menghasilkan perilaku yang konsisten pada contoh representatif.
Struktur repo yang memudahkan CI Python + Rust
Struktur repo yang rapi membantu menjaga pipeline tetap sederhana. Jika tim memakai Rust untuk engine inti dan Python untuk API atau notebook-facing layer, struktur berikut cukup praktis:
repo/
├─ crates/
│ └─ symbolic-core/
│ ├─ src/
│ └─ Cargo.toml
├─ python/
│ └─ symbolic_py/
│ ├─ src/symbolic_py/
│ ├─ tests/
│ └─ pyproject.toml
├─ tests/
│ ├─ fixtures/
│ │ └─ expressions.json
│ ├─ smoke/
│ │ ├─ python_smoke.py
│ │ └─ rust_smoke.rs
│ └─ determinism/
│ └─ canonical_cases.json
├─ .github/
│ └─ workflows/
│ └─ ci.yml
├─ rust-toolchain.toml
├─ Cargo.toml
├─ pyproject.toml
└─ MakefilePrinsip utamanya:
- fixture bersama disimpan di satu tempat agar Python dan Rust menguji kasus yang sama,
- kontrak hasil ditulis sebagai data, bukan hanya assert ad-hoc di test masing-masing bahasa,
- tooling file seperti
rust-toolchain.tomldan konfigurasi Python diletakkan di root agar versi tool lebih mudah dikunci.
Repositori mono-repo vs terpisah
Jika Python hanya menjadi binding tipis untuk core Rust, mono-repo biasanya lebih mudah karena:
- satu pipeline bisa memverifikasi perubahan lintas bahasa,
- fixture test dapat dibagi tanpa duplikasi,
- review lebih mudah melihat dampak perubahan algoritma ke wrapper Python.
Namun, mono-repo berarti CI perlu dioptimalkan agar perubahan kecil di satu sisi tidak selalu memicu job mahal di sisi lain. Gunakan path filter atau job condition bila perlu.
Aturan lint dan check yang relevan untuk kode simbolik
Linter umum tetap penting, tetapi untuk kode simbolik, Anda juga perlu aturan yang mendukung reproduksibilitas dan keterbacaan transformasi ekspresi.
Python: format, lint, dan type check
Pada Python, minimal gunakan tiga lapis berikut:
- formatter untuk konsistensi diff,
- linter untuk anti-pattern umum, impor tak terpakai, shadowing, atau blok kompleks,
- type checker untuk kontrak API yang menangani objek ekspresi, tree, token, atau hasil evaluasi.
Contoh target yang umum dipakai tim:
python -m ruff check python/
python -m ruff format --check python/
python -m mypy python/Hal yang sebaiknya diperiksa pada modul simbolik Python:
- fungsi yang menerima dan mengembalikan ekspresi punya tipe yang jelas,
- hindari penggunaan
Anydi layer transformasi inti, - jangan menyamakan string form dengan identitas matematis kecuali memang itu kontraknya,
- pisahkan fungsi parse, normalize, dan simplify agar test lebih presisi.
Rust: fmt, clippy, test, dan compile check
Di Rust, quality gate minimum biasanya mencakup:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo check --workspace --all-featuresUntuk kode simbolik, clippy berguna mendeteksi alokasi tidak perlu, clone berlebihan, atau pola iterator yang membuat normalisasi ekspresi menjadi lebih mahal dari yang terlihat. Tetapi jangan menjadikan semua saran clippy sebagai aturan absolut. Pada beberapa algoritma transformasi tree, keterbacaan lebih penting daripada optimisasi mikro.
Aturan domain-specific yang layak ditambahkan
Selain linter umum, pertimbangkan aturan internal berikut:
- larang assert berbasis string mentah untuk kesetaraan simbolik jika canonicalization belum dijamin,
- wajibkan canonical form sebelum snapshot atau perbandingan output,
- batasi penggunaan random seed implisit pada strategi simplifikasi atau property test,
- larang ketergantungan pada urutan map/hash untuk serialisasi hasil.
Aturan seperti ini biasanya diimplementasikan lewat review guideline, helper test, atau custom script sederhana, bukan linter generik.
Validasi determinisme hasil agar CI tidak flaky
Ini bagian yang paling sering diabaikan. Kode simbolik bisa benar secara matematis tetapi tetap tidak cocok untuk CI jika hasilnya tidak deterministik.
Apa yang dimaksud determinisme di sini
Determinisme berarti input yang sama, dengan versi tool dan konfigurasi yang sama, menghasilkan output yang sama secara konsisten. Output bisa berupa:
- bentuk kanonik ekspresi,
- hasil serialisasi ke string atau JSON,
- urutan term setelah normalisasi,
- hasil evaluasi numerik pada substitusi tertentu.
Kalau engine menganggap a+b dan b+a setara, Anda tetap perlu memutuskan apa bentuk keluarannya untuk test. Tanpa itu, snapshot test akan mudah pecah.
Pola test determinisme yang praktis
Gunakan fixture bersama berisi pasangan input dan output yang diharapkan dalam bentuk kanonik:
{
"cases": [
{
"input": "x + y + x",
"canonical": "2*x+y"
},
{
"input": "sin(a)^2 + cos(a)^2",
"canonical": "1"
}
]
}Di Python, smoke test bisa seperti ini:
import json
from pathlib import Path
from symbolic_py import parse_expr, canonicalize
def test_canonical_cases():
data = json.loads(Path("tests/determinism/canonical_cases.json").read_text())
for case in data["cases"]:
expr = parse_expr(case["input"])
assert canonicalize(expr).to_string() == case["canonical"]Di Rust, idenya sama: baca fixture yang sama, parse, canonicalize, lalu bandingkan ke bentuk keluaran yang sudah disepakati.
Jangan hanya menguji string, uji juga identitas matematis
Perbandingan string penting untuk stabilitas CI, tetapi tidak cukup. Tambahkan satu lapisan verifikasi identitas matematis, misalnya dengan substitusi beberapa nilai deterministik yang tetap aman untuk domain fungsi terkait. Tujuannya untuk menangkap kasus ketika canonical form stabil tetapi salah.
Catatan: substitusi numerik bukan bukti formal kesetaraan, tetapi cukup baik sebagai smoke test tambahan. Tetap utamakan aturan kanonik atau API kesetaraan simbolik bila library Anda menyediakannya.
Sumber non-determinisme yang umum
- iterasi pada struktur hash yang tidak menjamin urutan,
- penggunaan thread/paralelisme tanpa kontrol urutan reduksi,
- dependency native atau backend sistem yang berbeda antar runner,
- serialisasi yang berubah antar versi library,
- property-based test tanpa seed tetap saat dipakai sebagai quality gate utama.
Jika ada bagian yang memang sulit dibuat deterministik, bedakan test menjadi dua kelas: required dan informational. Jangan jadikan test flaky sebagai syarat merge.
Smoke test untuk ekspresi simbolik yang benar-benar berguna
Smoke test tidak perlu besar, tetapi harus mewakili jalur paling berisiko. Untuk symbolic code, pilih kasus yang menutup empat operasi dasar:
- parse input menjadi AST/IR,
- transformasi atau simplifikasi,
- serialisasi atau canonicalization,
- evaluasi atau substitusi sederhana.
Contoh skenario smoke test yang baik:
- ekspresi aritmetika dengan penggabungan term,
- identitas trigonometrik dasar,
- substitusi variabel dan evaluasi numerik,
- round-trip parse - canonicalize - serialize,
- kasus yang sebelumnya pernah menyebabkan bug produksi.
Jangan membuat smoke test terlalu luas. Tujuannya adalah mendeteksi kerusakan awal dengan waktu eksekusi rendah. Test yang mahal, misalnya simplifikasi besar atau benchmark regresi performa, sebaiknya dipisahkan ke workflow terjadwal atau job manual.
Cache dependency dan matrix build Python + Rust
Pipeline gabungan Python dan Rust mudah menjadi lambat jika setiap job mengunduh dependency dari awal. Cache perlu diatur, tetapi jangan sampai menyembunyikan problem build.
Prinsip cache yang aman
- ikat cache ke file lock atau metadata dependency,
- pisahkan cache Python dan Rust,
- jangan gunakan cache sebagai pengganti reproducible build,
- tetap sediakan satu jalur yang bisa berjalan tanpa cache untuk debugging.
Untuk Python, kunci dependency melalui file lock atau dependency manifest yang stabil. Untuk Rust, kunci dependency melalui Cargo.lock bila proyek Anda memang mengandalkannya untuk build reproducible.
Kapan perlu matrix build
Matrix build berguna jika tim perlu menjamin:
- binding Python tetap sehat di beberapa versi Python,
- crate Rust tetap lolos di beberapa target atau toolchain yang didukung,
- interaksi Python-Rust tidak pecah pada kombinasi tertentu.
Tetapi jangan membuat matrix terlalu lebar sejak awal. Untuk quality gate utama, biasanya cukup:
- satu job lint cepat,
- satu atau dua kombinasi build/test yang mewakili jalur utama,
- satu job matrix tambahan untuk kompatibilitas terbatas.
Kompatibilitas penuh dapat dijalankan pada jadwal malam atau sebelum rilis.
Contoh pipeline GitHub Actions untuk linting symbolic code Python/Rust di CI
Berikut contoh workflow yang menyeimbangkan kecepatan dan cakupan. Contoh ini tidak mengasumsikan library tertentu, tetapi pola quality gate-nya relevan untuk repositori symbolic code lintas bahasa.
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Python tooling
run: |
python -m pip install -U pip
pip install -e python/[dev] || pip install -r python/requirements-dev.txt
- name: Rust fmt and clippy
run: |
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: Python lint and type check
run: |
python -m ruff format --check python/
python -m ruff check python/
python -m mypy python/
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11']
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: Install Python deps
run: |
python -m pip install -U pip
pip install -e python/[dev] || pip install -r python/requirements-dev.txt
- name: Rust tests
run: cargo test --workspace --all-features
- name: Python tests
run: pytest -q python/tests tests/smoke
- name: Determinism checks
run: |
pytest -q tests/determinism
integration:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: Install deps
run: |
python -m pip install -U pip
pip install -e python/[dev] || pip install -r python/requirements-dev.txt
- name: Cross-language smoke test
run: |
python tests/smoke/python_smoke.py
cargo test -p symbolic-core smoke -- --nocaptureKenapa pipeline ini efektif:
- lint dipisah agar feedback cepat,
- test menjalankan matrix Python terbatas, bukan semua kombinasi yang mungkin,
- integration dijalankan setelah lint dan test lolos, sehingga job yang lebih mahal tidak membuang waktu saat basic gate sudah gagal.
Tips implementasi workflow
- Jika package Python bergantung pada build Rust, pastikan langkah instalasi memang membangun binding yang sama dengan yang dipakai test.
- Simpan fixture determinisme di jalur yang dibaca kedua bahasa.
- Gunakan
fail-fast: falsepada matrix saat Anda ingin melihat semua kombinasi yang gagal sekaligus. - Jangan campur benchmark performa ke workflow PR utama kecuali benar-benar ringan.
Risiko umum dan cara debug saat error sulit direproduksi
1. Hasil non-deterministik
Gejalanya: test snapshot atau canonicalization kadang lolos, kadang gagal. Langkah debug:
- pastikan ekspresi diurutkan dengan aturan yang eksplisit sebelum diserialisasi,
- cetak representasi internal minimal yang relevan, bukan hanya string akhir,
- jalankan test yang sama berkali-kali pada CI dan lokal untuk membedakan bug data race dari bug logika,
- matikan paralelisme sementara untuk isolasi masalah.
2. Versi tool berbeda antara developer dan CI
Gejalanya: formatter, hasil lint, atau serialisasi berubah tanpa perubahan kode berarti. Mitigasi:
- kunci toolchain Rust,
- dokumentasikan cara membuat environment Python yang konsisten,
- jalankan command yang sama di lokal dan CI melalui
make ciatau script seragam, - hindari mengandalkan tool global di mesin developer.
3. Error lintas bahasa sulit direproduksi
Contohnya, test Python gagal hanya saat memanggil binding Rust tertentu. Mitigasi yang berguna:
- buat satu command smoke lokal yang membangun Rust lalu menjalankan test Python inti,
- log versi Python, Rust, dan dependency penting di awal job CI,
- simpan satu fixture kecil yang memicu bug agar reproduksi tidak bergantung pada notebook atau data eksternal.
4. Cache menyembunyikan masalah build
Jika bug hanya muncul pada runner bersih, kemungkinan cache terlalu permisif. Solusinya:
- jalankan satu workflow tanpa cache secara berkala,
- pastikan key cache berubah saat lockfile berubah,
- hindari menyimpan artefak build yang tidak aman dipakai ulang antar kombinasi matrix yang berbeda.
Checklist adopsi bertahap agar quality gate tidak memperlambat release flow
Adopsi yang terlalu agresif sering membuat tim menonaktifkan CI karena dianggap menghambat. Lebih aman jika dilakukan bertahap:
- Fase 1: baseline
Tambahkan formatter, lint dasar, dan test smoke paling penting. Jangan langsung memblokir semua warning lama. - Fase 2: reproducibility
Tambahkan fixture determinisme untuk ekspresi inti dan kunci toolchain utama. - Fase 3: cross-language checks
Verifikasi bahwa Python dan Rust membaca fixture yang sama dan menghasilkan bentuk kanonik yang konsisten. - Fase 4: matrix terbatas
Uji beberapa versi Python atau kombinasi target yang benar-benar dipakai tim. - Fase 5: hardening
Pisahkan test mahal, benchmark, atau compatibility sweep ke workflow terjadwal agar PR tetap cepat.
Checklist operasional singkat
- Apakah repo punya fixture ekspresi simbolik bersama?
- Apakah canonicalization atau bentuk output stabil sudah didefinisikan?
- Apakah command lokal setara dengan command di CI?
- Apakah toolchain Python dan Rust cukup terkendali?
- Apakah test flaky dipisahkan dari gate wajib?
- Apakah cache aman dan terikat ke dependency state?
- Apakah job PR utama selesai cukup cepat untuk dipakai harian?
Penutup
Linting symbolic code Python/Rust di CI bukan hanya soal menambahkan ruff, mypy, atau clippy. Kunci build yang stabil ada pada definisi output kanonik, fixture bersama lintas bahasa, smoke test yang mewakili risiko nyata, dan kontrol toolchain agar hasil mudah direproduksi.
Jika tim mulai bereksperimen dengan engine simbolik atau library baru setelah tren seperti Symbolica 2.0, jangan langsung membuat pipeline yang terlalu kompleks. Mulailah dari gate yang murah dan deterministik, lalu tingkatkan cakupan secara bertahap. Dengan begitu, kualitas naik tanpa membuat alur rilis menjadi lambat atau rapuh.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!