Penguatan

Fuzzing, benchmarking, dan dokumentasi akhir proyek.

Daftar isi

Topik ini menguatkan proyek arsip-suhu yang sudah berjalan dengan tiga pekerjaan penutup, yaitu fuzzing yang mengawasi ketahanan parser terhadap masukan acak, benchmark struktural yang membandingkan dua struktur algoritme dengan satuan kerja yang deterministik, dan dokumentasi akhir berupa README yang ikut diuji serta dokumentasi rustdoc yang dibangun dari komentar kode. Seluruh berkas proyek pada tahap akhir disertakan utuh pada bundle topik ini sehingga hasilnya dapat dibangun ulang dari satu tempat. Setelah membacanya, penguatan serupa dapat diterapkan berurutan pada proyek lain, mulai dari pengawasan panic, pengukuran kerja, sampai penulisan dokumentasi yang ikut diverifikasi.

Keadaan akhir proyek#

Tahap ini menambah tiga berkas baru dan mengubah satu berkas lama, sedangkan seluruh berkas lainnya tetap seperti hasil sprint. Berkas baru adalah tests/fuzz_mini.rs sebagai fuzzer mini, examples/banding-lintasan.rs sebagai pengukuran struktural, dan README.md sebagai dokumentasi bagi pengunjung, sedangkan src/lib.rs bertambah tiga baris yang memasukkan contoh README ke dalam pengujian. Manifest di bawah tetap memuat feature async dengan tokio opsional dari sprint sebelumnya, sehingga pembangunan biasa tetap bebas dependensi eksternal.

text Struktur direktori proyek arsip-suhu tahap akhir
arsip-suhu/
├── Cargo.toml
├── README.md
├── catatan.txt
├── catatan-lama.txt
├── examples/
│   └── banding-lintasan.rs
├── src/
│   ├── lib.rs
│   ├── error.rs
│   ├── parser.rs
│   ├── statistik.rs
│   ├── main.rs
│   └── bin/
│       └── baca_async.rs
└── tests/
    ├── arsip.rs
    └── fuzz_mini.rs
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"]

Direktori examples/ berdiri berdampingan dengan tests/ sebagai wadah kode ilustratif, dan Cargo membangun isinya hanya ketika diminta lewat cargo run --example. Seluruh verifikasi pada topik ini kembali dilakukan pada proyek di luar pohon konten situs dengan berkas yang disalin dari bundle.

bash
# siapkan proyek penguatan yang bersih di luar pohon konten
rm -rf /tmp/proyek-arsip-penguatan
cargo new /tmp/proyek-arsip-penguatan -q --vcs none
# salin berkas proyek dari bundle ke posisinya masing-masing
cp Cargo.toml README.md /tmp/proyek-arsip-penguatan/
cp src/lib.rs /tmp/proyek-arsip-penguatan/src/lib.rs
cp src/error.rs /tmp/proyek-arsip-penguatan/src/error.rs
cp src/parser.rs /tmp/proyek-arsip-penguatan/src/parser.rs
cp src/statistik.rs /tmp/proyek-arsip-penguatan/src/statistik.rs
cp src/main.rs /tmp/proyek-arsip-penguatan/src/main.rs
mkdir -p /tmp/proyek-arsip-penguatan/src/bin /tmp/proyek-arsip-penguatan/tests /tmp/proyek-arsip-penguatan/examples
cp src/bin/baca_async.rs /tmp/proyek-arsip-penguatan/src/bin/baca_async.rs
cp tests/arsip.rs tests/fuzz_mini.rs /tmp/proyek-arsip-penguatan/tests/
cp examples/banding-lintasan.rs /tmp/proyek-arsip-penguatan/examples/
cp catatan.txt catatan-lama.txt /tmp/proyek-arsip-penguatan/

Fuzzing mini di atas parser#

Fuzzing menjalankan fungsi sasaran dengan masukan acak yang terus dibarukan sambil mengawasi apakah fungsi itu panic. Alat cargo fuzz menjalankan libFuzzer, yakni mesin fuzzing terbimbing cakupan (coverage-guided) yang membangkitkan masukan baru mengikuti jalur kode yang belum pernah dilalui. Pemasangan cargo fuzz memerlukan unduhan crate eksternal dan target sasarannya memerlukan toolchain nightly, sehingga pada lingkungan ini alat itu hanya ditampilkan sebagai cuplikan yang tidak dijalankan.

bash Menyiapkan dan menjalankan cargo fuzz
# tidak-dijalankan
cargo install cargo-fuzz
cargo fuzz init
cargo fuzz run urai_parser
rust Berkas fuzz/fuzz_targets/urai_parser.rs
// tidak-dijalankan
#![no_main]

use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    if let Ok(teks) = std::str::from_utf8(data) {
        arsip_suhu::parser::urai(teks);
    }
});

Pengawasan yang sama dapat dilakukan dengan pustaka standar secara deterministik, yaitu masukan acak dari linear congruential generator dengan benih tetap serta pengawas catch_unwind yang menangkap panic tanpa menghentikan program. Sasarannya adalah parser::urai berikut, yang memang dirancang tanpa pengindeksan dan tanpa unwrap, sehingga kegagalan sah seharusnya selalu keluar sebagai ArsipError, bukan panic.

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/error.rs
// tidak-dijalankan sendirian oleh rustc karena bagian dari pustaka arsip_suhu.

//! Tipe error untuk seluruh langkah pengolahan arsip.

use std::fmt;
use std::io;

/// Kegagalan yang terjadi saat memuat atau mengolah arsip catatan.
#[derive(Debug)]
pub enum ArsipError {
    /// Berkas tidak dapat dibaca oleh sistem berkas.
    BacaGagal {
        /// Error I/O dari sistem berkas.
        sumber: io::Error,
    },
    /// Baris tidak memuat tepat dua kolom atau suhunya bukan angka.
    BarisTidakSah {
        /// Nomor baris mulai dari satu.
        nomor: usize,
        /// Isi baris yang ditolak.
        baris: String,
    },
    /// Nilai suhu berada di luar jangkauan sensor.
    SuhuDiluarJangkauan {
        /// Nomor baris mulai dari satu.
        nomor: usize,
        /// Nilai suhu yang ditolak.
        nilai: f64,
    },
    /// Arsip tidak memuat satu pun catatan.
    TanpaData,
}

impl fmt::Display for ArsipError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ArsipError::BacaGagal { .. } => write!(f, "berkas tidak dapat dibaca"),
            ArsipError::BarisTidakSah { nomor, baris } => {
                write!(f, "baris ke-{nomor} tidak sah: \"{baris}\"")
            }
            ArsipError::SuhuDiluarJangkauan { nomor, nilai } => {
                write!(
                    f,
                    "suhu {nilai} pada baris ke-{nomor} di luar jangkauan -60..60"
                )
            }
            ArsipError::TanpaData => write!(f, "arsip tidak memuat catatan"),
        }
    }
}

impl std::error::Error for ArsipError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ArsipError::BacaGagal { sumber } => Some(sumber),
            _ => None,
        }
    }
}

impl From<io::Error> for ArsipError {
    fn from(sumber: io::Error) -> Self {
        ArsipError::BacaGagal { sumber }
    }
}

Berkas pengujian berikut membangun teks acak dari enam token tetap, termasuk token kosong yang menghasilkan baris kosong dan spasi beruntun. Pengujian memeriksa dua hal pada seribu masukan, yaitu parser tidak pernah panic, dan setiap masukan yang berhasil diurai menghasilkan tepat satu catatan untuk setiap baris tak kosongnya, karena baris yang tidak sah seharusnya sudah keluar sebagai error sebelum pencocokan jumlah berjalan.

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

//! Fuzzer mini: generator LCG dengan benih tetap menguji ketahanan parser.

use std::panic::{catch_unwind, AssertUnwindSafe};

use arsip_suhu::parser::urai;

const BANYAK_MASUKAN: u64 = 1000;

/// Satu langkah generator LCG (linear congruential generator).
fn langkah(benih: u64) -> u64 {
    benih
        .wrapping_mul(6364136223846793005)
        .wrapping_add(1442695040888963407)
}

/// Membangun teks masukan acak dari benih: satu sampai empat baris,
/// tiap baris nol sampai tiga token dari daftar tetap.
fn teks_acak(mut benih: u64) -> String {
    const TOKEN: [&str; 6] = ["2026-01-01", "26.5", "abc", "99.5", "-12.25", ""];
    let mut teks = String::new();
    let banyak_baris = ((benih >> 33) % 4 + 1) as usize;
    for _ in 0..banyak_baris {
        benih = langkah(benih);
        let banyak_token = ((benih >> 33) % 4) as usize;
        let mut kolom: Vec<&str> = Vec::new();
        for _ in 0..banyak_token {
            benih = langkah(benih);
            kolom.push(TOKEN[((benih >> 33) % TOKEN.len() as u64) as usize]);
        }
        teks.push_str(&kolom.join(" "));
        teks.push('\n');
    }
    teks
}

#[test]
fn parser_tahan_masukan_acak() {
    let mut benih: u64 = 2026;
    for _ in 0..BANYAK_MASUKAN {
        benih = langkah(benih);
        let teks = teks_acak(benih);
        let hasil = catch_unwind(AssertUnwindSafe(|| urai(&teks)));
        let Ok(hasil) = hasil else {
            panic!("parser panik pada masukan: {teks:?}");
        };
        // tiap baris tak kosong wajib menjadi catatan, karena baris tidak sah
        // sudah keluar sebagai error sebelum pencocokan ini berjalan
        if let Ok(catatan) = hasil {
            let baris_isi = teks.lines().filter(|l| !l.trim().is_empty()).count();
            assert_eq!(catatan.len(), baris_isi, "masukan: {teks:?}");
        }
    }
}

Integration test dari sprint tetap berada di tests/arsip.rs tanpa perubahan, dan fuzzer mini serta integration test berjalan dalam satu perintah pengujian.

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)));
}
bash
# jalankan seluruh pengujian termasuk fuzzer mini lalu buang segmen waktu yang berubah
cargo test --manifest-path /tmp/proyek-arsip-penguatan/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 1 test
test parser_tahan_masukan_acak ... ok

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

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

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

Harness fuzzer mini memuat parser_tahan_masukan_acak, dan harness terakhir kini memuat tiga doc test, termasuk satu contoh yang berasal dari README. Karena benihnya tetap, seribu masukan yang sama selalu diuji pada setiap jalankan, sehingga pengujian ini bekerja sebagai pengawas regresi, yaitu perubahan pada parser yang memunculkan panic pada salah satu masukan itu langsung menggagalkan pengujian.

Benchmark struktural lintasan#

Pengukuran pada tahap ini memakai satuan kerja, bukan waktu, karena waktu berubah antar mesin sedangkan jumlah kunjungan elemen tidak. Satu kunjungan didefinisikan sebagai satu kali iterator menyerahkan satu elemen, dan penghitungnya dipasang lewat adapter inspect sehingga kedua varian diukur dengan kode penghitung yang sama. Varian pertama menghitung total, tertinggi, dan terendah dalam satu lintasan seperti implementasi Statistik::dari pada pustaka berikut.

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)));
    }
}

Contoh pada direktori examples/ kemudian menulis ulang kedua struktur secara berdampingan, yaitu satu lintasan yang menghitung total sekaligus mencari maksimum dan minimum, serta tiga lintasan terpisah yang masing-masing menjumlahkan, mencari maksimum, lalu mencari minimum. Data ujinya dibangkitkan secara sintetis sebanyak 12.000 catatan tanpa membaca berkas, dan makro assert_eq! memeriksa silang hasil kedua varian sebelum jumlah kunjungannya dilaporkan.

rust examples/banding-lintasan.rs
// tidak-dijalankan sendirian oleh rustc karena memakai crate arsip_suhu dari berkas lain.

//! Perbandingan struktural: satu lintasan fold dibanding tiga lintasan terpisah.

use std::sync::atomic::{AtomicUsize, Ordering};

use arsip_suhu::parser::Catatan;

static KUNJUNGAN: AtomicUsize = AtomicUsize::new(0);

/// Membangun data uji deterministik tanpa membaca berkas.
fn catatan_sintetis(jumlah: usize) -> Vec<Catatan> {
    (0..jumlah)
        .map(|i| Catatan {
            tanggal: format!("2026-01-{:02}", i % 28 + 1),
            suhu: 20.0 + (i % 17) as f64 * 0.5,
        })
        .collect()
}

/// Statistik satu lintasan: total, tertinggi, dan terendah dalam satu putaran.
fn satu_lintasan(catatan: &[Catatan]) -> (f64, f64, f64) {
    let mut sisip = catatan.iter().inspect(|_| {
        KUNJUNGAN.fetch_add(1, Ordering::Relaxed);
    });
    let pertama = sisip.next().unwrap();
    let mut total = pertama.suhu;
    let mut tertinggi = pertama.suhu;
    let mut terendah = pertama.suhu;
    for c in sisip {
        total += c.suhu;
        tertinggi = tertinggi.max(c.suhu);
        terendah = terendah.min(c.suhu);
    }
    (total, tertinggi, terendah)
}

/// Statistik tiga lintasan: total, lalu tertinggi, lalu terendah.
fn tiga_lintasan(catatan: &[Catatan]) -> (f64, f64, f64) {
    let kunjungi = || {
        catatan.iter().inspect(|_| {
            KUNJUNGAN.fetch_add(1, Ordering::Relaxed);
        })
    };
    let total: f64 = kunjungi().map(|c| c.suhu).sum();
    let tertinggi = kunjungi().map(|c| c.suhu).fold(f64::NEG_INFINITY, f64::max);
    let terendah = kunjungi().map(|c| c.suhu).fold(f64::INFINITY, f64::min);
    (total, tertinggi, terendah)
}

fn main() {
    let catatan = catatan_sintetis(12_000);

    KUNJUNGAN.store(0, Ordering::Relaxed);
    let hasil_satu = satu_lintasan(&catatan);
    let kunjungan_satu = KUNJUNGAN.load(Ordering::Relaxed);

    KUNJUNGAN.store(0, Ordering::Relaxed);
    let hasil_tiga = tiga_lintasan(&catatan);
    let kunjungan_tiga = KUNJUNGAN.load(Ordering::Relaxed);

    assert_eq!(hasil_satu, hasil_tiga);
    println!("catatan       : {}", catatan.len());
    println!("satu lintasan : {kunjungan_satu} kunjungan");
    println!("tiga lintasan : {kunjungan_tiga} kunjungan");
}
bash
# jalankan pengukuran struktural lewat contoh cargo
cargo run --manifest-path /tmp/proyek-arsip-penguatan/Cargo.toml --example banding-lintasan -q
text Keluaran
catatan       : 12000
satu lintasan : 12000 kunjungan
tiga lintasan : 36000 kunjungan

Kedua varian menghasilkan statistik yang sama sehingga assert_eq! lolos, tetapi jumlah kunjungannya berbeda tiga kali lipat karena tiga lintasan mengunjungi setiap elemen tiga kali. Selisih inilah yang menjadi dasar keputusan mempertahankan Statistik::dari yang bekerja dalam satu lintasan, tanpa perlu mengukur waktu yang berubah antar mesin. Angka kunjungan juga dapat diverifikasi ulang oleh pembaca karena penghitungnya dipasang melalui adapter inspect yang sama pada kedua varian.

Dokumentasi akhir yang ikut diuji#

README menjadi pintu masuk proyek bagi pengunjung baru, sehingga contohnya ikut diikat ke pengujian agar tidak kedaluwarsa tanpa terdeteksi. Baris #[cfg(doctest)] beserta #[doc = include_str!("../README.md")] pada src/lib.rs menempelkan seluruh isi README sebagai dokumentasi pustaka, dan contoh Rust di dalamnya kemudian dijalankan sebagai doc test. Harness terakhir pada keluaran pengujian sebelumnya memuatnya sebagai ReadmeDoctests, sehingga setiap perubahan API yang merusak contoh README langsung menggagalkan cargo test.

markdown README.md
# arsip-suhu

Pengolah arsip catatan sensor suhu harian: membaca berkas teks, memvalidasi
setiap baris, lalu melaporkan statistik lengkapnya.

Setiap baris arsip memuat tanggal dan suhu yang dipisahkan spasi:

```text
2026-01-01 26.5
```

Contoh pemakaian sebagai pustaka:

```rust
use arsip_suhu::{SumberCatatan, Statistik};

let catatan = "2026-01-01 26.5\n2026-01-02 27.0".muat().unwrap();
let s = Statistik::dari(&catatan).unwrap();
assert_eq!(s.jumlah, 2);
assert_eq!(s.terendah, 26.5);
```

Program binernya menerima satu atau lebih berkas catatan:

```bash
cargo run -- catatan.txt catatan-lama.txt
```

Contoh pemakaian biner pada README di atas dapat dibuktikan langsung pada proyek yang sama. Perintah berikut masuk ke direktori proyek lalu menjalankan biner atas dua arsip contoh yang disalin pada tahap penyiapan, dan perintah cd pada sesi shell bertahan hingga perintah-perintah berikutnya.

bash
# masuk ke direktori proyek penguatan
cd /tmp/proyek-arsip-penguatan
# jalankan biner atas dua arsip contoh seperti pada README
cargo run -q -- catatan.txt catatan-lama.txt
text Keluaran
catatan.txt: 5 baris
catatan-lama.txt: 5 baris

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

Biner membaca kedua berkas pada utas terpisah di dalam lingkup terbatas, menggabungkan sepuluh catatannya, lalu mencetak statistik gabungannya. Rata-rata 25,73 derajat Celsius berasal dari pembagian total 257,3 atas sepuluh catatan, dan angka tersebut dapat dihitung ulang dari isi kedua berkas yang tersedia pada bundle. Terakhir, dokumentasi rustdoc dibangun dari komentar /// dan //! yang sudah tertulis pada seluruh modul sejak tahap arsitektur.

bash
# bangun dokumentasi rustdoc untuk pustaka ini saja
cargo doc --no-deps -q
# periksa halaman yang dihasilkan
ls target/doc/arsip_suhu
text Keluaran
all.html
error
index.html
parser
sidebar-items.js
statistik
trait.SumberCatatan.html

Setiap modul memperoleh satu halaman, dan trait SumberCatatan memperoleh halaman tersendiri yang memuat dua implementasinya beserta dokumentasi metodenya. Berkas all.html menggabungkan seluruh entri ke dalam satu halaman panjang, sedangkan sidebar-items.js memperlengkapi bilah samping pada browser. Dengan demikian ketiga pekerjaan penguatan selesai, dan proyek ditutup dalam keadaan yang dapat diuji, terukur, dan terbaca.

Ringkasan#

  • Fuzzer mini memakai linear congruential generator dengan benih tetap serta catch_unwind, sehingga seribu masukan acak yang sama diuji ulang pada setiap jalankan sebagai pengawas regresi atas parser.
  • Benchmark struktural mengukur jumlah kunjungan elemen alih-alih waktu, sehingga hasilnya stabil antar mesin dan selisih tiga kali lipat antara satu lintasan dan tiga lintasan dapat dijadikan dasar keputusan.
  • README diikat ke pengujian lewat #[doc = include_str!("../README.md")] dengan #[cfg(doctest)], sehingga contoh dokumentasi ikut dikompilasi dan dijalankan.
  • cargo doc --no-deps membangun dokumentasi rustdoc dari komentar kode tanpa berkas terpisah, dan biner tetap diverifikasi dari awal sampai akhir atas arsip contoh.

Referensi#