Miért lassú alapból a cargo test nagyobb projekteknél

A beépített cargo test egyetlen futtatható binárisba fordítja az összes tesztet egy crate-ből (integration és unit tesztek is), és ezt egy process-en belül, egy szálkészletben futtatja. Ez elsőre nem hangzik rossznak, de van pár bosszantó mellékhatása, ami nagyobb workspace-eknél igazán fájni kezd:

  • A kimenet lineáris és nehezen olvasható. Ha 500 teszt fut, és az 480. bukik el, addig görgetned kell, mire meglátod a hibát — vagy várnod kell a végére, mire kiírja az összefoglalót.
  • Egy panic az egész binárist megállíthatja, ha nem használsz #[should_panic]-ot vagy izolált process-eket, ami zajos, nehezen debuggolható állapotot eredményez.
  • A párhuzamosítás nem mindig optimális. A cargo test alapból is párhuzamosan futtatja a teszteket szálakon, de nincs finomhangolt slot-kezelés, retry logika, vagy per-teszt timeout.
  • Nagy workspace-eknél a build és a futtatás összefolyik, és nehéz átlátni, melyik crate teszteli épp mi.
Megjegyzés

A cargo test maga nem "rossz", csak egy bizonyos méret felett kényelmetlen. Kis projekteknél bőven elég.

Ezen problémákra született válasz a cargo-nextest, ami egy teljesen új teszt-futtató motor, miközben a te tesztjeid (#[test] függvények) egy sort sem változnak.

cargo-nextest telepítése és első futtatása

A telepítés a szokásos cargo install úton megy, vagy — ha gyorsabbat szeretnél — előre fordított binárisokat is letölthetsz a projekt GitHub release lapjáról.

cargo install cargo-nextest --locked

A --locked flag azért fontos, mert a nextest saját Cargo.lock-ját használja a fordításhoz, így garantáltan reprodukálható build-et kapsz.

A telepítés után egyetlen paranccsal lecserélheted a cargo test-et:

cargo nextest run

Ez a parancs automatikusan felismeri a workspace tagjait, lefordítja a teszt binárisokat, majd egy process-per-teszt modellben futtatja le mindegyiket. Ez a legfontosabb architekturális különbség: minden egyes teszt saját process-ben fut, nem osztott szálban.

Nézzünk egy egyszerű példát, amit ki is próbálhatsz:

// src/lib.rs
pub fn osszead(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn alap_osszeadas() {
        assert_eq!(osszead(2, 3), 5);
    }

    #[test]
    fn negativ_szamok() {
        assert_eq!(osszead(-4, -6), -10);
    }

    #[test]
    #[should_panic]
    fn tul_nagy_ertek() {
        let eredmeny = osszead(i32::MAX, 1);
        assert_eq!(eredmeny, i32::MIN); // ez itt csak demonstráció
    }
}

Ezt lefuttatva a cargo nextest run egy tisztább, színkódolt, per-teszt státuszú kimenetet ad, ahol azonnal látod, melyik teszt melyik crate-hez tartozik, és mennyi ideig futott.

Tipp

Ha csak egy adott teszt nevet szeretnél futtatni, ugyanaz a szűrési szintaxis működik, mint a cargo test-nél: cargo nextest run negativ.

Párhuzamosítás és riportolás összehasonlítása a beépített teszterrel

A legnagyobb gyakorlati különbség a process-izoláció. Ha egy teszt panic-kel vagy szegmentálási hibát (SIGSEGV) dob, a beépített cargo test-nél ez könnyen az egész binárist megzavarhatja, és nehéz megállapítani, hogy pontosan melyik teszt volt a ludas, ha több is fut egyszerre egy szálban. A nextest-nél minden teszt saját process — ha egy elhal, a többi zavartalanul folytatódik, és a riportban egyértelműen látod, melyik teszt bukott el, milyen kilépési kóddal.

Ennek van egy fontos velejárója is:

Figyelem

Mivel minden teszt külön process-ben fut, a tesztek közötti megosztott mutable állapot (pl. static mut, vagy fájlrendszeri erőforrások, amikre több teszt is ír) más módon viselkedhet, mint a cargo test-nél. Ha eddig burkoltan számítottál a közös memóriaterületre,érdemes átnézni a teszteket migráláskor.

A riportolás oldalán a nextest alapból is jóval informatívabb kimenetet ad: látod a lassú teszteket ("slow tests" szekció), a bukásokat kiemelve a végén összegyűjtve, és van beépített retry mechanizmus is instabil ("flaky") tesztekhez:

cargo nextest run --retries 2

Ezzel a beállítással egy elbukott teszt legfeljebb kétszer fut újra, mielőtt véglegesen hibásnak jelölnéd. Ez különösen hasznos, ha vannak időzítés-érzékeny integrációs teszteid.

A konfigurációt egy .config/nextest.toml fájlban is rögzíthetjük, hogy a csapat mindenkinél ugyanazokat a beállításokat használja:

[profile.default]
retries = 1
slow-timeout = { period = "30s", terminate-after = 2 }

Ezzel a projekt gyökerében elhelyezett fájllal biztosítod, hogy CI-ban és lokálisan is konzisztens legyen a viselkedés.

cargo-audit bemutatása: sebezhető crate-ek felderítése

Amíg a nextest a tesztelést gyorsítja, a cargo-audit a függőségeid biztonságát vizsgálja. A RustSec projekt Advisory Database-ét használja, ami folyamatosan frissülő, közösségi karbantartású adatbázis a Rust ecosystem ismert sebezhetőségeiről.

Telepítés:

cargo install cargo-audit --locked

Futtatás egy Cargo projektben:

cargo audit

A parancs beolvassa a Cargo.lock fájlt, és minden pontos verziójú függőséget összevet az advisory adatbázissal. Ha talál egyezést, kiírja a sebezhetőség azonosítóját (RUSTSEC-XXXX-XXXX formátumban), a súlyosságát, és — ha van — a javított verziót, amire érdemes upgrade-elni.

Jó tudni

A cargo audit a Cargo.lock fájlt vizsgálja, nem a Cargo.toml-t. Ha nincs commitolt lock fájl (pl. egy library crate-nél gyakori eset), előbb futtatnod kell egy cargo generate-lockfile parancsot, vagy egy build-et, hogy legyen mit ellenőrizni.

Ha a lock fájl naprakész, de az adatbázis változott (új CVE jelent meg), frissítened kell az advisory adatbázist is:

cargo audit --update

Ez alapból automatikusan is megtörténik minden futásnál, hacsak nem tiltod le a --no-fetch flaggel — offline environmentben (pl. légmentesen zárt CI runner) ez utóbbi hasznos lehet, ha előre letöltött másolatot használsz.

Mindkét eszköz integrálása egy egyszerű CI workflow-ba

A két eszköz remekül kiegészíti egymást egy CI pipeline-ban: a nextest gyorsítja a build-verify ciklust, az audit pedig biztonsági kaput ad minden pull request elé. Egy GitHub Actions workflow-ban ez így nézhet ki:

name: CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: taiki-e/install-action@nextest
      - name: Tesztek futtatása nextest-tel
        run: cargo nextest run --profile ci

  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: taiki-e/install-action@cargo-audit
      - name: Függőségek auditálása
        run: cargo audit

Az install-action egy közkedvelt megoldás, ami előre fordított binárisokat tölt le a megadott eszközökhöz, így nem kell minden futásnál a forrásból fordítani — ez jelentősen csökkenti a CI futási idejét mindkét eszköz esetén.

Tipp

Ha érzékeny vagy a build percekre, tedd külön job-ba a tesztelést és az auditot, mert így párhuzamosan futnak, és egy audit-hiba nem blokkolja a teszt job kimenetét (bár ez csapatpolitika kérdése — lehet, hogy pont azt szeretnéd, hogy blokkoljon).

Érdemes egy .config/nextest.toml-ban külön ci profilt is definiálni, ami esetleg más retry vagy timeout beállításokat használ, mint a lokális fejlesztői profil:

[profile.ci]
retries = 3
fail-fast = false

A fail-fast = false biztosítja, hogy a CI job ne álljon meg az első hibás tesztnél, hanem az összeset lefuttatva egyben lásd, mi bukott el — ez főleg nagyobb PR-eknél, sok módosított fájlnál hasznos.

Összefoglalás

A cargo-nextest és a cargo-audit két olyan eszköz, amit érdemes minél korábban beépíteni a workflow-dba, függetlenül attól, hogy hobbi projektet vagy production kódot írsz. A nextest process-izolációja stabilabb, gyorsabb és informatívabb tesztfuttatást ad, míg az audit egy apró, de kritikus biztonsági réteget húz a függőségeid fölé. A kettő kombinálva egy CI pipeline-ban minimális overhead-del jelentősen javítja a projekt megbízhatóságát — és mindkettő telepítése és beállítása kevesebb, mint tíz percet vesz igénybe.