Halo, Habr! Saya mempersembahkan kepada Anda terjemahan dari entri "# [test] pada tahun 2018" di blog John Renner, yang dapat ditemukan di 
sini .
Baru-baru ini, saya telah mengerjakan implementasi 
eRFC untuk kerangka pengujian khusus untuk Rust. Mempelajari basis kode kompiler, saya mempelajari internal pengujian di Rust dan menyadari bahwa akan menarik untuk membagikan ini.
Atribut # [tes]
Saat ini, programmer Rust mengandalkan atribut 
#[test] . Yang harus Anda lakukan adalah menandai fungsi sebagai tes dan mengaktifkan beberapa pemeriksaan:
 #[test] fn my_test() { assert!(2+2 == 4); } 
Ketika program ini dikompilasi menggunakan perintah 
cargo test rustc --test atau 
cargo test , itu akan membuat file yang dapat dieksekusi yang dapat menjalankan ini dan fungsi pengujian lainnya. Metode pengujian ini memungkinkan Anda untuk menjaga tes dekat dengan kode secara organik. Anda bahkan dapat memasukkan tes ke dalam modul pribadi:
 mod my_priv_mod { fn my_priv_func() -> bool {} #[test] fn test_priv_func() { assert!(my_priv_func()); } } 
Dengan demikian, entitas swasta dapat dengan mudah diuji tanpa menggunakan alat pengujian eksternal. Ini adalah kunci untuk pengujian ergonomis di Rust. Namun, secara semantik, ini agak aneh. Bagaimana fungsi 
main memanggil tes ini jika tidak terlihat ( 
catatan penerjemah : Saya ingatkan Anda, pribadi - dideklarasikan tanpa menggunakan kata kunci 
pub - dilindungi oleh enkapsulasi dari akses luar)? Apa sebenarnya yang dilakukan 
rustc --test ?
#[test] diimplementasikan sebagai konversi sintaksis di dalam 
libsyntax compiler libsyntax. Ini pada dasarnya adalah makro mewah yang menulis ulang peti kami dalam 3 langkah:
Langkah 1: Ekspor Kembali
Seperti yang disebutkan sebelumnya, tes dapat ada di dalam modul pribadi, jadi kita perlu cara untuk mengeksposnya ke fungsi 
main tanpa melanggar kode yang ada. Untuk tujuan ini, 
libsyntax membuat modul lokal yang disebut __test_reexports yang secara __test_reexports - __test_reexports tes . Pengungkapan ini menerjemahkan contoh di atas ke dalam:
 mod my_priv_mod { fn my_priv_func() -> bool {} fn test_priv_func() { assert!(my_priv_func()); } pub mod __test_reexports { pub use super::test_priv_func; } } 
Sekarang pengujian kami tersedia sebagai 
my_priv_mod::__test_reexports::test_priv_func . Untuk modul bersarang, 
__test_reexports akan 
__test_reexports modul yang berisi tes, sehingga tes 
a::b::my_test menjadi 
a::__test_reexports::b::__test_reexports::my_test . Sejauh ini proses ini tampaknya cukup aman, tetapi apa yang terjadi jika ada modul 
__test_reexports ada? Jawab: 
tidak ada .
Untuk menjelaskan, kita perlu memahami 
bagaimana AST mewakili pengidentifikasi . Nama setiap fungsi, variabel, modul, dll. disimpan bukan sebagai string, melainkan sebagai 
Simbol buram, yang pada dasarnya adalah nomor identifikasi untuk setiap pengidentifikasi. Compiler menyimpan tabel hash yang terpisah, yang memungkinkan kita untuk mengembalikan nama simbol yang dapat dibaca jika perlu (misalnya, ketika mencetak kesalahan sintaks). Ketika kompiler membuat modul 
__test_reexports , ia menghasilkan Simbol baru untuk pengenal, oleh karena itu, meskipun 
__test_reexports dihasilkan oleh kompiler mungkin memiliki nama yang sama dengan modul generik Anda, ia tidak akan menggunakan Simbolnya. Teknik ini mencegah tabrakan nama selama pembuatan kode dan merupakan dasar dari kebersihan sistem makro Rust.
Langkah 2: Menghasilkan Strapping
Sekarang tes kami dapat diakses dari akar peti kami, kami perlu melakukan sesuatu dengan mereka. 
libsyntax menghasilkan modul seperti itu:
 pub mod __test { extern crate test; const TESTS: &'static [self::test::TestDescAndFn] = &[]; #[main] pub fn main() { self::test::test_static_main(TESTS); } } 
Meskipun konversi ini sederhana, ini memberi kami banyak informasi tentang bagaimana tes sebenarnya dilakukan. Tes dikumpulkan ke dalam array dan diteruskan ke 
test_static_main uji, disebut 
test_static_main . Kami akan kembali ke apa itu 
TestDescAndFn , tetapi saat ini kesimpulan kuncinya adalah bahwa ada kotak yang disebut 
tes , yang merupakan bagian dari kernel Rust dan mengimplementasikan seluruh runtime untuk pengujian. Antarmuka 
test tidak stabil, oleh karena itu satu-satunya cara stabil untuk berinteraksi dengannya adalah 
#[test] makro 
#[test] .
Langkah 3: Membuat Objek Tes
Jika sebelumnya Anda menulis tes di Rust, Anda mungkin terbiasa dengan beberapa atribut opsional yang tersedia untuk fungsi tes. Misalnya, tes dapat dijelaskan dengan 
#[should_panic] jika kita mengharapkan tes menyebabkan kepanikan. Itu terlihat seperti ini:
 #[test] #[should_panic] fn foo() { panic!("intentional"); } 
Ini berarti bahwa pengujian kami lebih dari fungsi sederhana dan memiliki informasi konfigurasi. 
test mengkodekan data konfigurasi ini ke dalam struktur yang disebut 
TestDesc . Untuk setiap fungsi tes dalam peti, 
libsyntax akan menganalisis atributnya dan menghasilkan turunan 
TestDesc . Kemudian menggabungkan 
TestDesc dan fungsi tes ke dalam struktur logis 
TestDescAndFn , yang bekerja 
test_static_main . Untuk tes ini, instance 
TestDescAndFn dihasilkan terlihat seperti ini:
 self::test::TestDescAndFn { desc: self::test::TestDesc { name: self::test::StaticTestName("foo"), ignore: false, should_panic: self::test::ShouldPanic::Yes, allow_fail: false, }, testfn: self::test::StaticTestFn(|| self::test::assert_test_result(::crate::__test_reexports::foo())), } 
Setelah kami membuat larik objek uji ini, mereka akan diteruskan ke pelari uji melalui pengikatan yang dihasilkan pada langkah 2. Meskipun langkah ini dapat dianggap sebagai bagian dari langkah kedua, saya ingin menarik perhatian padanya sebagai konsep terpisah, karena ini akan menjadi kunci untuk menerapkan uji kustom kerangka kerja, tetapi ini akan menjadi posting blog lain.
Kata Penutup: Metode Penelitian
Walaupun saya mendapat banyak informasi langsung dari sumber kompiler, saya bisa mengetahui bahwa ada cara yang sangat sederhana untuk melihat apa yang dikerjakan kompiler. Kompilator nightly build memiliki tanda tidak stabil yang disebut 
unpretty , yang dapat Anda gunakan untuk mencetak kode sumber modul setelah memperluas makro:
 $ rustc my_mod.rs -Z unpretty=hir 
Catatan Penerjemah
Menarik untuk kepentingan, saya akan mengilustrasikan kode kasus uji setelah pengungkapan makro:
Kode Sumber Kustom:
 #[test] fn my_test() { assert!(2+2 == 4); } fn main() {} 
Kode setelah memperluas makro:
 #[prelude_import] use std::prelude::v1::*; #[macro_use] extern crate std as std; #[test] pub fn my_test() { if !(2 + 2 == 4) { { ::rt::begin_panic("assertion failed: 2 + 2 == 4", &("test_test.rs", 3u32, 3u32)) } }; } #[allow(dead_code)] fn main() { } pub mod __test_reexports { pub use super::my_test; } pub mod __test { extern crate test; #[main] pub fn main() -> () { test::test_main_static(TESTS) } const TESTS: &'static [self::test::TestDescAndFn] = &[self::test::TestDescAndFn { desc: self::test::TestDesc { name: self::test::StaticTestName("my_test"), ignore: false, should_panic: self::test::ShouldPanic::No, allow_fail: false, }, testfn: self::test::StaticTestFn(::__test_reexports::my_test), }]; }