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