مرحبا يا هبر! أقدم لكم ترجمة الإدخال "# [اختبار] في 2018" على مدونة John Renner ، والتي يمكن العثور عليها
هنا .
في الآونة الأخيرة ، كنت أعمل على تنفيذ
eRFC لأطر الاختبار المخصصة لـ Rust. بدراسة قاعدة كود المترجم ، درست الأجزاء الداخلية من الاختبار في Rust وأدركت أنه سيكون من المثير للاهتمام مشاركة هذا.
السمة # [اختبار]
اليوم ، يعتمد مبرمجو Rust على السمة المدمجة
#[test]
. كل ما عليك فعله هو تحديد الوظيفة كاختبار وتمكين بعض الفحوصات:
#[test] fn my_test() { assert!(2+2 == 4); }
عندما يتم تجميع هذا البرنامج باستخدام أوامر
cargo test
rustc --test
أو
cargo test
، فإنه سيقوم بإنشاء ملف قابل للتنفيذ يمكنه تشغيل هذا وأي وظيفة اختبار أخرى. تتيح لك طريقة الاختبار هذه الاحتفاظ بالاختبارات بشكل عضوي بالقرب من الشفرة. يمكنك أيضًا وضع الاختبارات داخل الوحدات الخاصة:
mod my_priv_mod { fn my_priv_func() -> bool {} #[test] fn test_priv_func() { assert!(my_priv_func()); } }
وبالتالي ، يمكن اختبار الكيانات الخاصة بسهولة دون استخدام أي أدوات اختبار خارجية. هذا هو المفتاح لاختبار مريح في الصدأ. دلالة ، ومع ذلك ، هذا غريب نوعا ما. كيف تستدعي الوظيفة
main
هذه الاختبارات إذا لم تكن مرئية (
ملاحظة المترجم : أذكرك ، خاص - تم إعلانه بدون استخدام الكلمة الرئيسية
pub
- محميًا بالتغليف من الوصول الخارجي)؟ ماذا يفعل
rustc --test
بالضبط؟
#[test]
تنفيذ
#[test]
كتحويل بناء داخل
libsyntax
مترجم libsyntax. هذا هو في الأساس ماكرو فاخر يعيد كتابة صندوقنا في 3 خطوات:
الخطوة 1: إعادة التصدير
كما ذكرنا سابقًا ، يمكن أن توجد الاختبارات داخل الوحدات الخاصة ، لذلك نحتاج إلى طريقة لتعريضهم للوظيفة
main
دون كسر الشفرة الحالية. تحقيقا لهذه الغاية ،
يقوم libsyntax
بإنشاء وحدات محلية تسمى __test_reexports
تقوم __test_reexports
الاختبارات بشكل متكرر . يترجم هذا الكشف المثال أعلاه إلى:
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; } }
يتوفر الآن
my_priv_mod::__test_reexports::test_priv_func
أنه
my_priv_mod::__test_reexports::test_priv_func
. بالنسبة للوحدات المتداخلة ، سيقوم
__test_reexports
الوحدات التي تحتوي على الاختبارات ، لذا فإن الاختبار
a::b::my_test
يصبح
a::__test_reexports::b::__test_reexports::my_test
. تبدو هذه العملية آمنة جدًا حتى الآن ، ولكن ماذا يحدث إذا كانت هناك وحدة
__test_reexports
موجودة؟ الجواب:
لا شيء .
للتوضيح ، نحن بحاجة إلى فهم
كيفية تمثيل AST للمعرفات . اسم كل وظيفة ، متغير ، وحدة ، إلخ. مخزنة ليس كسلسلة ، بل كرمز معتم ، وهو في الأساس رقم تعريف لكل معرف. يقوم المترجم بتخزين جدول تجزئة منفصل ، والذي يسمح لنا باستعادة الاسم الواضح للرمز إذا لزم الأمر (على سبيل المثال ، عند طباعة خطأ في بناء الجملة). عندما ينشئ المترجم وحدة
__test_reexports
، فإنه ينشئ رمزًا جديدًا
__test_reexports
، على الرغم من أن
__test_reexports
التي تم إنشاؤها بواسطة المترجم قد يكون لها نفس الاسم مع الوحدة النمطية العامة الخاصة بك ، فإنه لن يستخدم الرمز الخاص به. تمنع هذه التقنية تضارب الأسماء أثناء إنشاء الشفرة وهي أساس النظافة في نظام الماكرو الصدأ.
الخطوة 2: توليد الربط
الآن بعد أن يمكن الوصول إلى اختباراتنا من جذر صندوقنا ، نحتاج إلى القيام بشيء معهم. يولد
libsyntax
مثل هذه الوحدة:
pub mod __test { extern crate test; const TESTS: &'static [self::test::TestDescAndFn] = &[]; #[main] pub fn main() { self::test::test_static_main(TESTS); } }
على الرغم من أن هذا التحويل بسيط ، إلا أنه يعطينا الكثير من المعلومات حول كيفية إجراء الاختبارات فعليًا. يتم جمع الاختبارات في صفيف وتمريرها إلى
test_static_main
الاختبار ، تسمى
test_static_main
. سنعود إلى ما
TestDescAndFn
، ولكن في الوقت الحالي الاستنتاج الرئيسي هو أن هناك صندوق يسمى
اختبار ، وهو جزء من نواة Rust وينفذ وقت التشغيل بالكامل للاختبار. واجهة
test
غير مستقرة ، وبالتالي فإن الطريقة الثابتة الوحيدة للتفاعل معها هي الماكرو
#[test]
.
الخطوة 3: إنشاء كائن اختبار
إذا كنت قد كتبت اختبارات سابقة في Rust ، فقد تكون على دراية ببعض السمات الاختيارية المتاحة لوظائف الاختبار. على سبيل المثال ، يمكن وضع تعليق توضيحي على الاختبار
#[should_panic]
إذا كنا نتوقع أن يتسبب الاختبار في حالة من الذعر. يبدو شيء مثل هذا:
#[test] #[should_panic] fn foo() { panic!("intentional"); }
هذا يعني أن اختباراتنا هي أكثر من مجرد وظائف بسيطة ولديها معلومات التكوين. يشفر
test
بيانات التكوين هذه في بنية تسمى
TestDesc . لكل وظيفة اختبار في الصندوق ، سيقوم
libsyntax
بتحليل سماته وإنشاء مثيل لـ
TestDesc
. ثم يجمع بين
TestDesc
ووظيفة الاختبار في البنية المنطقية
TestDescAndFn
، والتي تعمل
test_static_main
. بالنسبة لهذا الاختبار ، يبدو مثيل
TestDescAndFn
كما يلي:
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())), }
بمجرد إنشاء مجموعة من كائنات الاختبار هذه ، يتم تمريرها إلى مشغل الاختبار عبر الربط الذي تم إنشاؤه في الخطوة 2. على الرغم من أن هذه الخطوة يمكن اعتبارها جزءًا من الخطوة الثانية ، أود أن ألفت الانتباه إليها كمفهوم منفصل ، لأن هذا سيكون مفتاح تنفيذ اختبار مخصص الأطر ، ولكن ستكون هذه مشاركة مدونة أخرى.
خاتمة: طرق البحث
على الرغم من أنني حصلت على الكثير من المعلومات مباشرة من مصادر المترجم ، فقد تمكنت من معرفة أن هناك طريقة بسيطة جدًا لمعرفة ما يفعله المترجم. يحتوي بناء المحول البرمجي الليلي على علامة غير مستقرة تسمى غير
unpretty
، والتي يمكنك استخدامها لطباعة التعليمات البرمجية المصدر للوحدة النمطية بعد توسيع وحدات الماكرو:
$ rustc my_mod.rs -Z unpretty=hir
ملاحظة المترجم
من المثير للاهتمام ، سأوضح رمز حالة الاختبار بعد الإفصاح الكلي:
رمز المصدر المخصص:
#[test] fn my_test() { assert!(2+2 == 4); } fn main() {}
الرمز بعد توسيع وحدات الماكرو:
#[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), }]; }