حول جهاز وظائف الاختبار المدمجة في Rust (ترجمة)

مرحبا يا هبر! أقدم لكم ترجمة الإدخال "# [اختبار] في 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), }]; } 

Source: https://habr.com/ru/post/ar418095/


All Articles