Sprint Implementasi

Iterasi pembangunan dengan clippy dan pengujian.

Daftar isi

Topik ini membangun proyek arsip-suhu melalui empat sprint, yaitu satuan kerja yang masing-masing menghasilkan proyek yang utuh dan lulus pemeriksaan. Sprint pertama menambatkan kode dengan pengujian unit dan integrasi, sprint kedua menambahkan pemrosesan beberapa arsip dalam utas terbatas sehingga ownership data benar-benar berpindah antar utas, sprint ketiga menambahkan varian pembacaan async dengan tokio pada tepi I/O, dan sprint keempat memasang gerbang clippy serta fmt di atas seluruh hasilnya. Setelah membacanya, pola kerja per iterasi yang selalu diakhiri verifikasi dapat diterapkan pada proyek lain, beserta alasan tiap keputusan konkurensinya.

Sprint pertama: pengujian menyeluruh#

Modul parser dan statistik dari tahap arsitektur kini menambatkan perilakunya dengan unit test di dalam modul #[cfg(test)]. Pengujian parser menyasar empat tepi aturan bentuk baris, yaitu baris kosong yang dilewati, jumlah kolom yang salah, suhu yang bukan angka, dan suhu di luar jangkauan. Pengujian statistik menyasar daftar satu elemen yang membuat seluruh nilai sama, serta daftar kosong yang wajib ditolak dengan TanpaData.

rust src/parser.rs
// tidak-dijalankan sendirian oleh rustc karena bagian dari pustaka arsip_suhu.

//! Penguraian teks catatan sensor menjadi daftar [`Catatan`].

use crate::error::ArsipError;

/// Satu baris catatan harian: tanggal dan suhu.
#[derive(Debug, Clone, PartialEq)]
pub struct Catatan {
    /// Tanggal sebagaimana tertulis, misalnya "2026-01-01".
    pub tanggal: String,
    /// Suhu dalam derajat Celsius.
    pub suhu: f64,
}

/// Jangkauan suhu yang dianggap masuk akal bagi sensor ruangan.
const JANGKAUAN: f64 = 60.0;

/// Menguraikan teks menjadi daftar catatan.
///
/// Setiap baris memuat tanggal dan suhu yang dipisahkan spasi, contohnya
/// `2026-01-01 26.5`. Baris kosong dilewati, sedangkan baris berisi kolom
/// selain dua, suhu bukan angka, atau suhu di luar jangkauan menghentikan
/// penguraian dengan error yang menyebut nomor barisnya.
///
/// ```
/// use arsip_suhu::parser::urai;
///
/// let catatan = urai("2026-01-01 26.5\n\n2026-01-02 27.0").unwrap();
/// assert_eq!(catatan.len(), 2);
/// assert_eq!(catatan[0].tanggal, "2026-01-01");
/// assert_eq!(catatan[0].suhu, 26.5);
/// ```
pub fn urai(teks: &str) -> Result<Vec<Catatan>, ArsipError> {
    let mut hasil = Vec::new();
    for (i, mentah) in teks.lines().enumerate() {
        let baris = mentah.trim();
        if baris.is_empty() {
            continue;
        }
        let mut kolom = baris.split_whitespace();
        let (tanggal, suhu) = match (kolom.next(), kolom.next(), kolom.next()) {
            (Some(tanggal), Some(suhu), None) => (tanggal, suhu),
            _ => return Err(tidak_sah(i + 1, baris)),
        };
        let suhu: f64 = match suhu.parse() {
            Ok(nilai) => nilai,
            Err(_) => return Err(tidak_sah(i + 1, baris)),
        };
        if !(-JANGKAUAN..=JANGKAUAN).contains(&suhu) {
            return Err(ArsipError::SuhuDiluarJangkauan {
                nomor: i + 1,
                nilai: suhu,
            });
        }
        hasil.push(Catatan {
            tanggal: tanggal.to_string(),
            suhu,
        });
    }
    Ok(hasil)
}

/// Membungkus baris mentah menjadi error dengan nomor barisnya.
fn tidak_sah(nomor: usize, baris: &str) -> ArsipError {
    ArsipError::BarisTidakSah {
        nomor,
        baris: baris.to_string(),
    }
}

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

    #[test]
    fn urai_melewati_baris_kosong() {
        let catatan = urai("2026-01-01 26.5\n   \n2026-01-02 27.0\n").unwrap();
        assert_eq!(catatan.len(), 2);
    }

    #[test]
    fn urai_menolak_tiga_kolom() {
        let hasil = urai("2026-01-01 26.5 catatan");
        assert!(matches!(
            hasil,
            Err(ArsipError::BarisTidakSah { nomor: 1, .. })
        ));
    }

    #[test]
    fn urai_menolak_suhu_bukan_angka() {
        let hasil = urai("2026-01-01 hangat");
        assert!(matches!(
            hasil,
            Err(ArsipError::BarisTidakSah { nomor: 1, .. })
        ));
    }

    #[test]
    fn urai_menolak_suhu_di_luar_jangkauan() {
        let hasil = urai("2026-01-01 99.5");
        assert!(matches!(
            hasil,
            Err(ArsipError::SuhuDiluarJangkauan {
                nomor: 1,
                nilai: 99.5
            })
        ));
    }
}
rust src/statistik.rs
// tidak-dijalankan sendirian oleh rustc karena bagian dari pustaka arsip_suhu.

//! Statistik arsip: jumlah, rata-rata, tertinggi, dan terendah.

use std::fmt;

use crate::error::ArsipError;
use crate::parser::Catatan;

/// Ringkasan statistik atas sekumpulan catatan.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Statistik {
    /// Banyaknya catatan yang dihitung.
    pub jumlah: usize,
    /// Nilai rata-rata seluruh suhu.
    pub rata_rata: f64,
    /// Suhu tertinggi.
    pub tertinggi: f64,
    /// Suhu terendah.
    pub terendah: f64,
}

impl Statistik {
    /// Menghitung statistik dalam satu lintasan atas daftar catatan.
    ///
    /// Daftar kosong menghasilkan error `TanpaData` karena rata-rata,
    /// tertinggi, dan terendah tidak memiliki makna tanpa satu pun nilai.
    ///
    /// ```
    /// use arsip_suhu::parser::urai;
    /// use arsip_suhu::statistik::Statistik;
    ///
    /// let catatan = urai("2026-01-01 26.0\n2026-01-02 28.0").unwrap();
    /// let s = Statistik::dari(&catatan).unwrap();
    /// assert_eq!(s.jumlah, 2);
    /// assert_eq!(s.tertinggi, 28.0);
    /// assert_eq!(s.terendah, 26.0);
    /// ```
    pub fn dari(catatan: &[Catatan]) -> Result<Statistik, ArsipError> {
        let mut sisip = catatan.iter();
        let Some(pertama) = sisip.next() else {
            return Err(ArsipError::TanpaData);
        };
        let mut jumlah = 1;
        let mut total = pertama.suhu;
        let mut tertinggi = pertama.suhu;
        let mut terendah = pertama.suhu;
        for c in sisip {
            jumlah += 1;
            total += c.suhu;
            tertinggi = tertinggi.max(c.suhu);
            terendah = terendah.min(c.suhu);
        }
        Ok(Statistik {
            jumlah,
            rata_rata: total / jumlah as f64,
            tertinggi,
            terendah,
        })
    }
}

impl fmt::Display for Statistik {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "jumlah baris : {}", self.jumlah)?;
        writeln!(f, "rata-rata    : {:.2} °C", self.rata_rata)?;
        writeln!(f, "tertinggi    : {:.2} °C", self.tertinggi)?;
        write!(f, "terendah     : {:.2} °C", self.terendah)
    }
}

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

    fn contoh(suhu: f64) -> Catatan {
        Catatan {
            tanggal: String::from("2026-01-01"),
            suhu,
        }
    }

    #[test]
    fn statistik_satu_catatan() {
        let s = Statistik::dari(&[contoh(26.5)]).unwrap();
        assert_eq!(s.jumlah, 1);
        assert_eq!(s.rata_rata, 26.5);
        assert_eq!(s.tertinggi, 26.5);
        assert_eq!(s.terendah, 26.5);
    }

    #[test]
    fn statistik_daftar_kosong() {
        let hasil = Statistik::dari(&[]);
        assert!(matches!(hasil, Err(ArsipError::TanpaData)));
    }
}

Integration test pada direktori tests/ memakai pustaka sebagai pemakai luar melalui API publiknya. Lima pengujian di dalamnya menguji jalur yang luput dari unit test, yaitu trait SumberCatatan untuk teks dan untuk Path, statistik atas arsip contoh beserta rata-rata pecahannya, penolakan baris satu kolom pada nomor baris kedua, dan arsip tanpa data. Pembandingan rata-rata memakai selisih di bawah satu per miliar karena nilai pecahan biner hasil pembagian jarang sama persis dengan tulisan desimalnya.

rust tests/arsip.rs
// tidak-dijalankan sendirian oleh rustc karena memakai crate arsip_suhu sebagai pustaka eksternal.

//! Pengujian integrasi atas API publik arsip-suhu.

use std::path::Path;

use arsip_suhu::error::ArsipError;
use arsip_suhu::parser::urai;
use arsip_suhu::{Statistik, SumberCatatan};

const TEKS_ARSIP: &str =
    "2026-01-01 24.1\n2026-01-02 25.6\n2026-01-03 26.5\n2026-01-04 28.4\n2026-01-05 27.3\n";

#[test]
fn sumber_str_memuat_lima_catatan() {
    let catatan = TEKS_ARSIP.muat().unwrap();
    assert_eq!(catatan.len(), 5);
    assert_eq!(catatan[0].tanggal, "2026-01-01");
    assert_eq!(catatan[4].suhu, 27.3);
}

#[test]
fn statistik_arsip_contoh() {
    let catatan = urai(TEKS_ARSIP).unwrap();
    let s = Statistik::dari(&catatan).unwrap();
    assert_eq!(s.jumlah, 5);
    assert_eq!(s.tertinggi, 28.4);
    assert_eq!(s.terendah, 24.1);
    assert!((s.rata_rata - 26.38).abs() < 1e-9);
}

#[test]
fn sumber_path_melaporkan_berkas_hilang() {
    let hasil = Path::new("/tidak-ada/arsip.txt").muat();
    assert!(matches!(hasil, Err(ArsipError::BacaGagal { .. })));
}

#[test]
fn urai_menolak_baris_satu_kolom() {
    let hasil = urai("2026-01-01 26.5\ntanggal-saja");
    assert!(matches!(
        hasil,
        Err(ArsipError::BarisTidakSah { nomor: 2, .. })
    ));
}

#[test]
fn statistik_tanpa_data() {
    let catatan = urai("\n").unwrap();
    let hasil = Statistik::dari(&catatan);
    assert!(matches!(hasil, Err(ArsipError::TanpaData)));
}

Eksekusi seluruh pengujian dilakukan pada proyek di luar pohon konten situs. Segmen waktu pada baris rangkuman disaring dengan sed karena nilainya berubah setiap kali pengujian dijalankan, dan opsi --test-threads=1 menjaga urutan baris tetap stabil.

bash
# siapkan proyek sprint yang bersih di luar pohon konten
rm -rf /tmp/proyek-arsip-sprint
cargo new /tmp/proyek-arsip-sprint -q --vcs none
# salin berkas proyek dari bundle ke posisinya masing-masing
cp Cargo.toml /tmp/proyek-arsip-sprint/Cargo.toml
cp src/lib.rs /tmp/proyek-arsip-sprint/src/lib.rs
cp src/error.rs /tmp/proyek-arsip-sprint/src/error.rs
cp src/parser.rs /tmp/proyek-arsip-sprint/src/parser.rs
cp src/statistik.rs /tmp/proyek-arsip-sprint/src/statistik.rs
cp src/main.rs /tmp/proyek-arsip-sprint/src/main.rs
mkdir -p /tmp/proyek-arsip-sprint/src/bin /tmp/proyek-arsip-sprint/tests
cp src/bin/baca_async.rs /tmp/proyek-arsip-sprint/src/bin/baca_async.rs
cp tests/arsip.rs /tmp/proyek-arsip-sprint/tests/arsip.rs
cp catatan.txt catatan-lama.txt /tmp/proyek-arsip-sprint/
# jalankan seluruh pengujian secara berurutan lalu buang segmen waktu yang berubah
cargo test --manifest-path /tmp/proyek-arsip-sprint/Cargo.toml -- --test-threads=1 2>/dev/null | sed 's/; finished in [0-9.]*s$//'
text Keluaran
running 6 tests
test parser::tests::urai_melewati_baris_kosong ... ok
test parser::tests::urai_menolak_suhu_bukan_angka ... ok
test parser::tests::urai_menolak_suhu_di_luar_jangkauan ... ok
test parser::tests::urai_menolak_tiga_kolom ... ok
test statistik::tests::statistik_daftar_kosong ... ok
test statistik::tests::statistik_satu_catatan ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

running 5 tests
test statistik_arsip_contoh ... ok
test statistik_tanpa_data ... ok
test sumber_path_melaporkan_berkas_hilang ... ok
test sumber_str_memuat_lima_catatan ... ok
test urai_menolak_baris_satu_kolom ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

running 2 tests
test src/parser.rs - parser::urai (line 26) ... ok
test src/statistik.rs - statistik::Statistik::dari (line 29) ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Laporan memuat empat harness berturut-turut. Enam unit test berjalan pada crate pustaka, nol pengujian pada biner karena main.rs memang tidak memuat fungsi pengujian, lima integration test pada crate terpisah, lalu dua doc test yang dikumpulkan dari contoh dokumentasi. Setiap iterasi berikutnya wajib mempertahankan keluaran ini tetap hijau sebelum fitur baru ditambahkan.

Sprint kedua: beberapa arsip dalam utas terbatas#

Biner sebelumnya hanya membaca satu berkas, dan iterasi ini melebarkannya menjadi daftar berkas dengan thread::scope. Setiap berkas dibaca dan diurai pada utas sendiri, lalu seluruh hasilnya digabungkan berurutan sebelum statistik dihitung. Data ujinya bertambah satu arsip lama sehingga penggabungan dua sumber benar-benar terlihat pada laporan.

text catatan.txt
2026-01-01 24.1
2026-01-02 25.6
2026-01-03 26.5
2026-01-04 28.4
2026-01-05 27.3
text catatan-lama.txt
2025-12-27 23.2
2025-12-28 24.8
2025-12-29 26.1
2025-12-30 25.4
2025-12-31 25.9
rust src/main.rs
// tidak-dijalankan sendirian oleh rustc karena memakai crate pustaka arsip_suhu dari berkas lain.

//! Biner arsip-suhu: membaca beberapa arsip sekaligus lalu mencetak laporan.

use std::path::PathBuf;

use arsip_suhu::{ArsipError, Statistik, SumberCatatan};

fn main() {
    if let Err(e) = jalankan() {
        laporan(&e);
        std::process::exit(1);
    }
}

/// Mencetak error beserta rantai penyebabnya dari titik paling luar.
fn laporan(e: &ArsipError) {
    eprintln!("error: {e}");
    let mut sumber = std::error::Error::source(e);
    while let Some(s) = sumber {
        eprintln!("sebab: {s}");
        sumber = s.source();
    }
}

fn jalankan() -> Result<(), ArsipError> {
    let jalur: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
    if jalur.is_empty() {
        eprintln!("pemakaian: arsip-suhu <berkas-catatan> [berkas...]");
        std::process::exit(2);
    }

    // setiap berkas dibaca dan diurai pada utas sendiri di dalam lingkup terbatas
    let per_berkas = std::thread::scope(|lingkup| -> Vec<Result<Vec<_>, ArsipError>> {
        let tugas: Vec<_> = jalur
            .iter()
            .map(|j| lingkup.spawn(move || j.muat()))
            .collect();
        tugas
            .into_iter()
            .map(|t| t.join().expect("utas pengurai tidak panik"))
            .collect()
    });

    let mut semua = Vec::new();
    let mut ringkas = Vec::new();
    for (j, hasil) in jalur.iter().zip(per_berkas) {
        let catatan = hasil?;
        ringkas.push((j.display().to_string(), catatan.len()));
        semua.extend(catatan);
    }
    for (nama, n) in &ringkas {
        println!("{nama}: {n} baris");
    }
    println!();
    println!("{}", Statistik::dari(&semua)?);
    Ok(())
}

Utas di dalam thread::scope meminjam daftar jalur, bukan menerima salinannya, karena masa hidup lingkup menjamin seluruh utas selesai sebelum pinjaman itu berakhir. Closure move pada lingkup.spawn memindahkan referensi &PathBuf itu sendiri, dan setiap utas mengembalikan Result lengkap sehingga kegagalan tidak menghentikan utas lain secara mendadak. Hasil semua utas dikumpulkan sebagai Vec menurut urutan argumen, kemudian digabungkan satu per satu, sehingga error yang dilaporkan selalu milik berkas pertama yang gagal menurut urutan tersebut, apa pun urutan utas yang benar-benar selesai lebih dulu.

utas utamadaftar jalurutas 1: baca + uraicatatan-lama.txtutas 2: baca + uraicatatan.txtutas 3: baca + uraiarsip lainpenggabungan berurutanlalu Statistik::darithread::scope meminjamkan daftar jalur tanpa Arc, lalu join terjadi sebelum scope berakhir
Pembacaan beberapa arsip dalam utas terbatas thread::scope

Pemanggilan t.join().expect(...) menganggap utas pengurai tidak pernah panic, dan asumsi itu sah karena fungsi yang dipanggil mengembalikan Result alih-alih menggugurkan dirinya. Laporan percobaan berikut memuat baris ringkas per berkas terurut, diikuti statistik gabungan yang menggabungkan sepuluh catatan dari dua arsip.

bash
# jalankan biner utama terhadap dua arsip sekaligus
cargo run --manifest-path /tmp/proyek-arsip-sprint/Cargo.toml -q -- catatan-lama.txt catatan.txt
text Keluaran
catatan-lama.txt: 5 baris
catatan.txt: 5 baris

jumlah baris : 10
rata-rata    : 25.73 °C
tertinggi    : 28.40 °C
terendah     : 23.20 °C

Jalur kegagalan diuji dengan berkas yang memuat satu baris rusak. Berkas itu dibuat di luar pohon konten, dan program berhenti dengan pesan yang menyebut nomor baris serta isinya.

bash
# siapkan berkas dengan satu baris rusak untuk menguji jalur error
printf '2026-01-01 26.5\nbaris rusak\n' > /tmp/proyek-arsip-sprint/rusak.txt
# jalankan biner utama terhadap berkas rusak tersebut
cargo run --manifest-path /tmp/proyek-arsip-sprint/Cargo.toml -q -- /tmp/proyek-arsip-sprint/rusak.txt 2>&1
text Keluaran
error: baris ke-2 tidak sah: "baris rusak"

Sprint ketiga: varian async pada tepi I/O#

Konkurensi utas pada sprint kedua cukup untuk program kecil, tetapi pengerjaan proyek ini juga menuntut satu titik yang memakai async agar polanya terlatih pada data nyata. Varian itu dibuat sebagai biner kedua yang membaca berkas lewat tokio::fs, karena pustaka standar belum menyediakan operasi berkas async. Manifest menambahkan feature async dengan tokio sebagai dependensi opsional, sehingga pembangunan biasa tetap tanpa dependensi eksternal sama sekali.

toml Cargo.toml
[package]
name = "arsip-suhu"
version = "0.1.0"
edition = "2021"
default-run = "arsip-suhu"

[features]
async = ["dep:tokio"]

[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"], optional = true }

[[bin]]
name = "arsip-suhu-async"
path = "src/bin/baca_async.rs"
required-features = ["async"]

Tulisan dep:tokio pada deklarasi feature mengikat tokio sebagai dependensi opsional, dan required-features = ["async"] pada tabel [[bin]] membuat biner kedua hanya dibangun ketika feature itu aktif. Dengan pengaturan tersebut, cargo test dan cargo run biasa tidak pernah menyentuh tokio, sedangkan varian async dibangun atas permintaan lewat --features async.

rust src/bin/baca_async.rs
// tidak-dijalankan sendirian oleh rustc karena memakai crate tokio sebagai dependensi.

//! Biner arsip-suhu-async: varian pembacaan berkas serentak dengan tokio.

use std::path::PathBuf;

use arsip_suhu::{ArsipError, Statistik, SumberCatatan};

#[tokio::main]
async fn main() {
    if let Err(e) = jalankan().await {
        eprintln!("error: {e}");
        let mut sumber = std::error::Error::source(&e);
        while let Some(s) = sumber {
            eprintln!("sebab: {s}");
            sumber = s.source();
        }
        std::process::exit(1);
    }
}

async fn jalankan() -> Result<(), ArsipError> {
    let jalur: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
    if jalur.is_empty() {
        eprintln!("pemakaian: arsip-suhu-async <berkas-catatan> [berkas...]");
        std::process::exit(2);
    }

    // pembacaan tiap berkas menjadi task tokio yang berjalan serentak
    let mut tugas = Vec::new();
    for j in &jalur {
        let j = j.clone();
        tugas.push(tokio::spawn(
            async move { tokio::fs::read_to_string(j).await },
        ));
    }

    let mut semua = Vec::new();
    let mut ringkas = Vec::new();
    for (j, t) in jalur.iter().zip(tugas) {
        let teks = t.await.expect("task pembaca tidak panik")?;
        let catatan = teks.as_str().muat()?;
        ringkas.push((j.display().to_string(), catatan.len()));
        semua.extend(catatan);
    }
    for (nama, n) in &ringkas {
        println!("{nama}: {n} baris");
    }
    println!();
    println!("{}", Statistik::dari(&semua)?);
    Ok(())
}

Pembacaan berkas adalah satu-satunya bagian async pada program ini. Setiap jalur di-clone lalu dipindahkan ke dalam task tokio::spawn karena task tidak dijamin menempel pada satu utas, dan hasil pembacaannya dikumpulkan sebagai JoinHandle yang di-await menurut urutan argumen. Teks yang sudah dibaca kemudian diurai lewat implementasi str dari trait SumberCatatan, sehingga inti pengolahan tetap kode sinkron yang sama dan tidak ada logika domain yang diduplikasi ke versi async.

bash
# jalankan varian async dengan feature async dan biner kedua
cargo run --manifest-path /tmp/proyek-arsip-sprint/Cargo.toml -q --features async --bin arsip-suhu-async -- catatan-lama.txt catatan.txt
text Keluaran
catatan-lama.txt: 5 baris
catatan.txt: 5 baris

jumlah baris : 10
rata-rata    : 25.73 °C
tertinggi    : 28.40 °C
terendah     : 23.20 °C

Keluarannya identik dengan biner utas pada sprint kedua karena kedua varian memakai parser dan statistik yang sama, dan perbedaannya hanya pada cara menunggu pembacaan berkas. Titik await pada t.await mengembalikan kendali ke runtime selama disk bekerja, dan itulah seluruh perbedaan perilaku async pada program ini.

Sprint keempat: gerbang clippy dan fmt#

Iterasi terakhir tidak menambah fitur, melainkan memasang gerbang yang menjaga seluruh kode tetap sehat. Perintah clippy dengan -D warnings menaikkan seluruh peringatan menjadi error sehingga pemeriksaan gagal bila masih ada temuan, dan opsi --all-targets --features async memastikan biner async ikut diperiksa, bukan hanya pustaka dan biner utama. Pemeriksaan format dengan cargo fmt --check memastikan seluruh berkas mengikuti tata letak baku rustfmt.

bash
# periksa seluruh target termasuk biner async, lalu jadikan peringatan sebagai error
cargo clippy --manifest-path /tmp/proyek-arsip-sprint/Cargo.toml --features async --all-targets -q -- -D warnings
# periksa format seluruh berkas Rust pada proyek
cargo fmt --manifest-path /tmp/proyek-arsip-sprint/Cargo.toml --check

Kedua perintah selesai tanpa mencetak apa pun karena tidak ada temuan tersisa. Keadaan itu bukan hasil pembersihan di akhir, melainkan kebiasaan sepanjang sprint, karena kode yang ditulis rapi sejak awal membuat gerbang hanya mengonfirmasi alih-alih memaksa penulisan ulang. Gerbang seperti inilah yang layak dipasang pada pipeline pemeriksaan otomatis ketika proyek tumbuh lebih besar.

Kesalahan umum#

Lupa default-run setelah menambah biner kedua. Perintah cargo run menolak memilih biner begitu paket memuat lebih dari satu, dan kegagalan itu baru terlihat saat menjalankan perintah harian. Tetapkan biner bawaan pada manifest, lalu biarkan biner tambahan dipilih eksplisit dengan --bin.

Memperlakukan join().expect sebagai penanganan error. Penggabungan utas yang menggugurkan program ketika utas pekerja panic adalah keputusan, bukan pemulihan, karena panic di satu utas meruntuhkan seluruh proses. Utas pekerja pada proyek ini mengembalikan Result sehingga panic memang tidak pernah diharapkan, dan program yang membutuhkan ketahanan tingkat utas sebaiknya memakai catch_unwind pada utas pekerjanya.

Memindahkan seluruh inti program ke async. Mengurai teks dan menghitung statistik tidak menunggu apa pun, sehingga menulis ulang keduanya sebagai async hanya menambah kompleksitas tanpa manfaat. Batasi async pada operasi yang benar-benar menunggu, misalnya pembacaan berkas dan jaringan, lalu biarkan inti pengolahan tetap sinkron dan dapat diuji seperti biasa.

Ringkasan#

  • Sprint diakhiri pengujian yang lulus, sehingga setiap penambahan fitur berangkat dari keadaan yang sehat dan dapat dibandingkan.
  • thread::scope meminjamkan daftar jalur ke beberapa utas pengurai tanpa Arc, dan hasil digabungkan menurut urutan argumen agar error yang dilaporkan deterministik.
  • Feature async dengan dep:tokio dan required-features menambah biner tokio tanpa membebani pembangunan biasa, dan inti pengolahan tetap sinkron lewat trait SumberCatatan.
  • Gerbang cargo clippy -q --all-targets -- -D warnings beserta cargo fmt --check menjaga mutu tanpa mencetak apa pun ketika seluruh temuan bersih.

Referensi#