Lewati ke konten

Dokumentasi dan Doc Test di Rust

Setiap halaman di docs.rs dan setiap halaman dokumentasi standard library ditulis oleh seorang programmer, langsung di dalam kode sumber, memakai komentar khusus. Rust mengubah komentar-komentar itu menjadi sebuah website hanya dengan satu perintah. Kamu bisa melakukan hal yang sama persis untuk kodemu sendiri.

Ada alasan kedua kenapa ini penting. Contoh kode di dalam dokumentasimu adalah tes sungguhan. Saat kamu menjalankan cargo test, Rust meng-compile dan menjalankannya. Kalau kamu mengubah fungsi lalu lupa memperbarui contohnya, tesnya gagal. Dokumentasimu tidak bisa diam-diam jadi usang. Ini jarang ada di bahasa pemrograman lain, dan termasuk salah satu hal paling menyenangkan dari Rust.

Kamu sudah bertemu komentar biasa di halaman Komentar. Doc comment (komentar dokumentasi) mirip, tapi memakai tiga karakter:

  • /// mendokumentasikan item di bawahnya: fungsi, struct, enum, field, atau modul.
  • //! mendokumentasikan item tempat ia berada. Di bagian paling atas src/lib.rs atau src/main.rs, ia menjelaskan seluruh crate.
//! Program resep yang mungil.
/// Membagi `total` gram secara merata menjadi `portions` porsi.
fn split_evenly(total: u32, portions: u32) -> u32 {
total / portions
}

Baris pertama doc comment sebaiknya berupa satu kalimat ringkasan yang pendek. Berbagai tool menampilkan baris itu di daftar dan hasil pencarian, jadi tulislah sebaik mungkin.

Doc comment tidak mengubah apa yang dilakukan programmu. Berikut fungsi yang sudah didokumentasikan dan dipakai dari main:

/// Membagi `total` gram secara merata menjadi `portions` porsi.
///
/// Sisa gramnya dibuang, jadi 10 gram dibagi 3 porsi hasilnya 3.
///
/// # Panics
///
/// Panic jika `portions` bernilai nol.
fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("portions harus lebih besar dari nol");
}
total / portions
}
fn main() {
println!("{}", split_evenly(600, 3));
println!("{}", split_evenly(10, 3));
}

Output:

200
3
▶ Coba sendiri

Di kebanyakan editor, kalau kamu mengarahkan kursor ke split_evenly di dalam main, teks ini muncul di pop-up. Itu saja sudah sepadan dengan usahanya.

Doc comment ditulis dalam Markdown, format yang sama dengan yang dipakai di GitHub dan banyak aplikasi chat. Kamu bisa memakai `code`, **tebal**, *miring*, daftar, link, dan judul:

/// Mengembalikan suhu oven untuk sebuah masakan, dalam **derajat Celsius**.
///
/// Masakan yang didukung:
///
/// - `"bread"`: 220 derajat
/// - `"cake"`: 180 derajat
/// - selain itu: 200 derajat
///
/// *Selalu* cek oven kamu sendiri, karena setiap oven berbeda.
/// Lihat [Celsius di Wikipedia](https://id.wikipedia.org/wiki/Celsius).
pub fn oven_temperature(dish: &str) -> u32 {
match dish {
"bread" => 220,
"cake" => 180,
_ => 200,
}
}

Sisipkan baris /// kosong di antara paragraf, sama seperti baris kosong di file Markdown biasa. Bungkus nama fungsi, tipe, dan nilai dengan backtick supaya tampil sebagai kode.

Programmer Rust memakai beberapa judul yang sama berulang kali, supaya pembaca tahu harus melihat ke mana. Kamu sudah melihat salah satunya. Judul-judul ini biasanya ditulis dalam bahasa Inggris, karena begitulah konvensinya di seluruh ekosistem Rust. Ini yang paling umum:

  • # Examples: cara memakai item tersebut. Hampir setiap item publik sebaiknya punya bagian ini.
  • # Panics: kapan fungsi bisa panic, supaya pemanggil bisa menghindarinya.
  • # Errors: kapan fungsi yang mengembalikan Result memberikan Err, dan kenapa.
  • # Safety: untuk unsafe fn, aturan yang wajib dipatuhi pemanggil (lihat Unsafe Rust).

Berikut # Errors dan # Safety di sebuah file library, src/lib.rs:

/// Membaca takaran seperti `"250g"` dan mengembalikan jumlah gramnya.
///
/// # Errors
///
/// Mengembalikan error jika teksnya tidak diakhiri `g`,
/// atau jika bagian sebelumnya bukan bilangan bulat.
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' harus diakhiri g"))?;
number
.parse()
.map_err(|_| format!("'{number}' bukan bilangan bulat"))
}
/// Mengembalikan bahan pertama tanpa memeriksa apakah daftarnya kosong.
///
/// # Safety
///
/// Pemanggil harus memastikan `ingredients` tidak kosong.
/// Memanggil fungsi ini pada slice kosong adalah undefined behavior.
pub unsafe fn first_unchecked(ingredients: &[String]) -> &String {
unsafe { ingredients.get_unchecked(0) }
}

Perhatikan bahwa judulnya memakai satu #. Di dalam doc comment, # adalah judul Markdown, bukan atribut Rust.

Di sisa halaman ini kita akan mengerjakan sebuah library kecil. Buat dengan cargo new --lib kitchen. Lalu, di dalam proyek itu, jalankan:

Terminal window
cargo doc --open
Documenting kitchen v0.1.0 (/path/to/kitchen)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s
Generated /path/to/kitchen/target/doc/kitchen/index.html

Cargo membangun sebuah website HTML di target/doc/, dan --open membukanya di browser. Tampilannya persis seperti halaman di docs.rs: ada sidebar, kotak pencarian, dan satu halaman untuk setiap fungsi dan tipe. Cargo juga mendokumentasikan setiap crate yang kamu pakai sebagai dependensi, yang sangat berguna saat kamu sedang offline.

Secara default, hanya item publik (yang diberi pub) yang muncul, karena dokumentasi ini ditujukan untuk orang yang memakai library-mu. Tambahkan --document-private-items untuk menyertakan semuanya.

Doc test: contoh yang benar-benar dijalankan

Section titled “Doc test: contoh yang benar-benar dijalankan”

Berikut bagian awal src/lib.rs di library kitchen. Fungsinya punya bagian # Examples berisi blok kode di dalam doc comment:

//! Fungsi-fungsi bantu kecil untuk dapur.
//!
//! `kitchen` mengonversi suhu oven, membagi makanan menjadi beberapa porsi,
//! dan membaca takaran seperti `"250g"`.
/// Mengonversi suhu dari Celsius ke Fahrenheit.
///
/// # Examples
///
/// ```
/// let f = kitchen::celsius_to_fahrenheit(180.0);
/// assert_eq!(f, 356.0);
/// ```
pub fn celsius_to_fahrenheit(celsius: f64) -> f64 {
celsius * 9.0 / 5.0 + 32.0
}

Sekarang jalankan cargo test:

Doc-tests kitchen
running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo menemukan blok kode di baris 10, mengubahnya menjadi program kecil, meng-compile, lalu menjalankannya. assert_eq! memeriksa jawabannya. Ada beberapa hal yang perlu diperhatikan:

  • Contohnya memanggil kitchen::celsius_to_fahrenheit, lengkap dengan nama crate. Doc test di-compile seolah-olah ia adalah program milik orang lain yang memakai library-mu, jadi ia hanya bisa melihat item pub, dan kamu perlu path lengkap atau baris use kitchen::...;.
  • Kamu tidak perlu menulis fn main. Rustdoc membungkus contohmu di dalam main secara otomatis.
  • Pakai assert! dan assert_eq! (dari halaman Testing) untuk menunjukkan sekaligus memeriksa hasilnya.

Bagaimana kalau contohnya salah? Ganti 356.0 menjadi 350.0, lalu jalankan tesnya lagi:

running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... FAILED
failures:
---- src/lib.rs - celsius_to_fahrenheit (line 10) stdout ----
Test executable failed (exit status: 101).
stderr:
assertion `left == right` failed
left: 356.0
right: 350.0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Pesan dari Rust memang selalu dalam bahasa Inggris. Di sini ia bilang assertion left == right gagal: nilai sebenarnya (left) adalah 356.0, sedangkan yang ditulis di contoh (right) adalah 350.0.

Itulah intinya. Contoh yang salah langsung ketahuan, alih-alih membingungkan pembaca berbulan-bulan kemudian. Untuk menjalankan doc test saja, pakai cargo test --doc.

Kadang sebuah contoh butuh baris-baris yang bisa mengalihkan perhatian pembaca. Baris di doc test yang diawali # (tanda pagar dan spasi) tetap di-compile, tapi tidak ditampilkan di dokumentasi yang dihasilkan.

Pemakaian paling umum: kamu ingin menunjukkan operator ?, tapi ? hanya bisa dipakai di dalam fungsi yang mengembalikan Result. Jadi kamu menyembunyikan main buatanmu sendiri:

/// Membaca takaran seperti `"250g"` dan mengembalikan jumlah gramnya.
///
/// # Examples
///
/// ```
/// use kitchen::parse_grams;
///
/// # fn main() -> Result<(), String> {
/// let flour = parse_grams("250g")?;
/// assert_eq!(flour, 250);
///
/// assert!(parse_grams("banyak").is_err());
/// # Ok(())
/// # }
/// ```
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' harus diakhiri g"))?;
number
.parse()
.map_err(|_| format!("'{number}' bukan bilangan bulat"))
}

Di browser, pembaca hanya melihat empat baris yang menarik: use, pemanggilan dengan ?, dan dua assert. Tesnya tetap meng-compile semuanya.

Tidak semua contoh bisa langsung dijalankan begitu saja. Kamu bisa menaruh sebuah kata setelah backtick pembuka untuk memberi tahu rustdoc apa yang harus dilakukan:

  • ```no_run: compile contohnya, tapi jangan jalankan. Cocok untuk kode yang membaca file, membuka koneksi jaringan, atau berulang selamanya.
  • ```should_panic: jalankan dan harapkan terjadi panic. Tesnya gagal kalau ternyata tidak panic.
  • ```ignore: jangan di-compile sama sekali. Pakai untuk sketsa dan pseudo-code. Coba no_run dulu, karena contoh yang di-ignore bisa jadi usang tanpa ada yang sadar.
  • ```text: sama sekali bukan kode Rust, cuma teks (misalnya contoh output).
/// Membagi `total` gram secara merata menjadi `portions` porsi.
///
/// # Examples
///
/// ```
/// assert_eq!(kitchen::split_evenly(600, 3), 200);
/// ```
///
/// Nol porsi tidak masuk akal, jadi kode ini panic:
///
/// ```should_panic
/// kitchen::split_evenly(600, 0);
/// ```
pub fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("portions harus lebih besar dari nol");
}
total / portions
}
/// Memuat daftar belanja dari file, satu barang per baris.
///
/// ```no_run
/// let items = kitchen::load_list("shopping.txt").unwrap();
/// println!("Ada {} barang yang harus dibeli", items.len());
/// ```
pub fn load_list(path: &str) -> std::io::Result<Vec<String>> {
let text = std::fs::read_to_string(path)?;
Ok(text.lines().map(String::from).collect())
}

Dengan semua contoh ini di kitchen, ditambah satu sketsa ignore di komentar //! milik crate, cargo test melaporkan:

running 7 tests
test src/lib.rs - (line 8) ... ignored
test src/lib.rs - load_list (line 87) - compile ... ok
test src/lib.rs - split_evenly (line 34) ... ok
test src/lib.rs - split_evenly (line 40) - should panic ... ok
test src/lib.rs - parse_grams (line 59) ... ok
test src/lib.rs - celsius_to_fahrenheit (line 16) ... ok
test src/lib.rs - Recipe (line 104) ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01s

Tes no_run bertuliskan - compile: contoh itu hanya di-build. Urutan baris-barisnya bisa berubah setiap kali dijalankan, karena tes berjalan secara paralel.

Di dalam doc comment, tulis sebuah nama di dalam backtick dan kurung siku, seperti [`Vec`], dan rustdoc mengubahnya menjadi link ke halaman item tersebut. Ini disebut intra-doc link. Cara ini bisa dipakai untuk item buatanmu sendiri maupun apa pun di standard library:

//! Program resep yang mungil.
/// Sebuah resep: nama dan daftar bahan beserta beratnya dalam gram.
///
/// Buat dengan [`Recipe::new`] lalu isi dengan [`Recipe::add`].
/// Bahan-bahannya disimpan dalam [`Vec`], sesuai urutan kamu menambahkannya.
struct Recipe {
/// Nama yang ditampilkan di bagian atas resep.
name: String,
/// Setiap bahan beserta beratnya dalam gram.
ingredients: Vec<(String, u32)>,
}
impl Recipe {
/// Membuat resep kosong dengan nama yang diberikan.
fn new(name: &str) -> Recipe {
Recipe { name: name.to_string(), ingredients: Vec::new() }
}
/// Menambahkan bahan ke akhir daftar.
fn add(&mut self, ingredient: &str, grams: u32) {
self.ingredients.push((ingredient.to_string(), grams));
}
/// Menjumlahkan berat semua bahan.
fn total_grams(&self) -> u32 {
self.ingredients.iter().map(|(_, grams)| grams).sum()
}
}
fn main() {
let mut pancakes = Recipe::new("Panekuk");
pancakes.add("tepung", 200);
pancakes.add("susu", 300);
pancakes.add("telur", 50);
println!("{}", pancakes.name);
for (ingredient, grams) in &pancakes.ingredients {
println!("- {ingredient}: {grams} g");
}
println!("Total: {} g", pancakes.total_grams());
}

Output:

Panekuk
- tepung: 200 g
- susu: 300 g
- telur: 50 g
Total: 550 g
▶ Coba sendiri

Field dan method juga bisa punya komentar /// sendiri, dan masing-masing muncul di halaman struct tersebut. Kalau sebuah link menunjuk ke sesuatu yang tidak ada, cargo doc akan memperingatkanmu. Di sini Recipe::push adalah salah ketik dari Recipe::add:

warning: unresolved link to `Recipe::push`
--> src/lib.rs:100:51
|
100 | /// Buat dengan [`Recipe::new`] lalu isi dengan [`Recipe::push`].
| ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push`
|
= note: `#[warn(rustdoc::broken_intra_doc_links)]` on by default

Artinya: link ke Recipe::push tidak bisa ditemukan, karena struct Recipe tidak punya field atau item bernama push.

Mewajibkan dokumentasi dengan missing_docs

Section titled “Mewajibkan dokumentasi dengan missing_docs”

Untuk library yang akan dipakai orang lain, kamu bisa meminta compiler mengingatkanmu tentang setiap item publik yang belum didokumentasikan. Taruh baris ini di bagian paling atas src/lib.rs:

#![warn(missing_docs)]

Sekarang, lupa menulis doc comment akan memunculkan peringatan:

warning: missing documentation for a function
--> src/lib.rs:138:1
|
138 | pub fn grams_to_ounces(grams: f64) -> f64 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: the lint level is defined here
--> src/lib.rs:1:9
|
1 | #![warn(missing_docs)]
| ^^^^^^^^^^^^

Peringatan ini bilang ada fungsi tanpa dokumentasi, dan note di bawahnya menunjukkan di mana pemeriksaan ini dinyalakan. #! (dengan tanda seru) berarti atributnya berlaku untuk seluruh crate. Pemeriksaan ini juga meminta komentar //! untuk crate itu sendiri. Item privat tidak pernah dilaporkan, jadi fungsi bantu internal tidak akan merepotkanmu.

docs.rs: hosting gratis untuk dokumentasimu

Section titled “docs.rs: hosting gratis untuk dokumentasimu”

Saat kamu memublikasikan crate ke crates.io (lihat Cargo dan Crate), docs.rs otomatis membangun dokumentasinya dan meng-host-nya secara gratis, untuk setiap versi. Kamu tidak perlu melakukan apa-apa. Itulah sebabnya hampir setiap crate Rust punya dokumentasi yang tampilannya seragam dan mudah dicari.

Dua tips lagi:

  • Jalankan rustup doc --std untuk membuka dokumentasi standard library secara offline di browser.
  • Di halaman docs.rs atau std mana pun, klik link Source untuk membaca kode di balik sebuah fungsi. Ini cara belajar yang sangat bagus.

Lupa menulis nama crate di doc test. Doc test berada di luar crate-mu, jadi kode ini gagal:

error[E0425]: cannot find function `celsius_to_fahrenheit` in this scope
--> src/lib.rs:17:9
|
17 | let f = celsius_to_fahrenheit(180.0);
| ^^^^^^^^^^^^^^^^^^^^^ not found in this scope

Compiler tidak menemukan fungsi celsius_to_fahrenheit di scope (cakupan) contoh itu. Solusinya: tulis kitchen::celsius_to_fahrenheit(180.0), atau tambahkan baris use kitchen::celsius_to_fahrenheit; di bagian atas contoh (boleh disembunyikan dengan # kalau mau).

Doc comment yang tidak mendokumentasikan apa pun. /// harus berada tepat sebelum sebuah item. Di dalam badan fungsi, pakai //:

error[E0585]: found a documentation comment that doesn't document anything
--> src/main.rs:3:5
|
3 | /// Bagi makanan menjadi tiga porsi.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: doc comments must come before what they document, if a comment was intended use `//`

Compiler menemukan doc comment yang tidak menjelaskan apa-apa. Bagian help mengingatkan bahwa doc comment harus ditulis sebelum hal yang didokumentasikannya, dan kalau yang kamu maksud adalah komentar biasa, pakai //.

Mengira doc test di program biner ikut dijalankan. Doc test hanya berjalan untuk crate library (src/lib.rs). Contoh di doc comment src/main.rs tidak dites. Kalau programmu makin besar, pindahkan logikanya ke src/lib.rs dan biarkan main.rs tetap kecil.

Menulis dokumentasi yang cuma mengulang kode. “Menjumlahkan dua angka” di atas fn add(a, b) tidak membantu. Jelaskan hal yang tidak terlihat oleh pembaca: satuan (gram? detik?), kasus-kasus khusus, dan kapan fungsinya gagal.

  • /// mendokumentasikan item berikutnya, dan //! mendokumentasikan crate atau modul tempat ia berada. Keduanya memakai Markdown.
  • Pakai bagian-bagian standar: # Examples, # Panics, # Errors, dan # Safety.
  • cargo doc --open membangun situs HTML untuk crate-mu beserta dependensinya.
  • Blok kode di doc comment adalah doc test: cargo test meng-compile dan menjalankannya. Sembunyikan baris persiapan dengan # , dan pakai no_run, should_panic, atau ignore bila perlu.
  • [`Name`] membuat intra-doc link, dan #![warn(missing_docs)] mengingatkanmu tentang item publik yang belum didokumentasikan.
  • Crate yang dipublikasikan ke crates.io otomatis mendapat hosting dokumentasi di docs.rs.

Kuis singkat

1. Komentar mana yang mendokumentasikan seluruh crate jika ditaruh di bagian atas src/lib.rs?

2. Apa yang dilakukan cargo test terhadap blok kode di dalam komentar /// sebuah library?

3. Di sebuah doc test, apa fungsi baris yang diawali # (tanda pagar dan spasi)?

4. Sebuah contoh membuka koneksi jaringan. Kamu ingin contoh itu di-compile tapi tidak dijalankan. Anotasi mana yang kamu pakai?

Skor: 0 / 4