Über das Gerät der eingebauten Testfunktionalität in Rust (Übersetzung)

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Eintrags "# [test] in 2018" auf John Renners Blog, den Sie hier finden .

Vor kurzem habe ich an der Implementierung von eRFC für benutzerdefinierte Test-Frameworks für Rust gearbeitet. Als ich die Codebasis des Compilers studierte, studierte ich die Interna des Testens in Rust und erkannte, dass es interessant wäre, dies zu teilen.

Attribut # [Test]


Heute verlassen sich Rust-Programmierer auf das integrierte Attribut #[test] . Sie müssen lediglich die Funktion als Test markieren und einige Überprüfungen aktivieren:

 #[test] fn my_test() { assert!(2+2 == 4); } 

Wenn dieses Programm mit den rustc --test oder rustc --test kompiliert wird, wird eine ausführbare Datei erstellt, in der diese und alle anderen Testfunktionen ausgeführt werden können. Mit dieser Testmethode können Sie Tests organisch in der Nähe des Codes halten. Sie können sogar Tests in privaten Modulen durchführen:

 mod my_priv_mod { fn my_priv_func() -> bool {} #[test] fn test_priv_func() { assert!(my_priv_func()); } } 

Somit können private Entitäten einfach ohne Verwendung externer Testwerkzeuge getestet werden. Dies ist der Schlüssel zu ergonomischen Tests in Rust. Semantisch ist dies jedoch ziemlich seltsam. Wie ruft die main diese Tests auf, wenn sie nicht sichtbar sind ( Anmerkung des Übersetzers : Ich erinnere Sie daran, dass privat - ohne Verwendung des Schlüsselworts pub deklariert - durch Kapselung vor Zugriff von außen geschützt sind)? Was genau macht rustc --test ?

#[test] als Syntaxkonvertierung in der libsyntax-Compilerkiste implementiert. Dies ist im Wesentlichen ein ausgefallenes Makro, das unsere Kiste in drei Schritten umschreibt:

Schritt 1: Reexport


Wie bereits erwähnt, können Tests in privaten Modulen vorhanden sein. Daher benötigen wir eine Möglichkeit, sie der main zugänglich zu machen, ohne den vorhandenen Code zu beschädigen. Zu diesem Zweck erstellt libsyntax lokale Module mit dem Namen __test_reexports , die Tests rekursiv __test_reexports . Diese Offenbarung übersetzt das obige Beispiel in:

 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; } } 

Jetzt ist unser Test als my_priv_mod::__test_reexports::test_priv_func . Bei verschachtelten Modulen __test_reexports Module, die die Tests enthalten, erneut, sodass der Test a::b::my_test zu a::__test_reexports::b::__test_reexports::my_test . Dieser Prozess scheint bisher ziemlich sicher zu sein, aber was passiert, wenn ein __test_reexports Modul vorhanden ist? Antwort: nichts .

Um dies zu erklären, müssen wir verstehen, wie der AST Bezeichner darstellt . Der Name jeder Funktion, Variablen, jedes Moduls usw. nicht als Zeichenfolge gespeichert, sondern als undurchsichtiges Symbol , das im Wesentlichen eine Identifikationsnummer für jede Kennung darstellt. Der Compiler speichert eine separate Hash-Tabelle, mit der wir bei Bedarf den lesbaren Namen des Symbols wiederherstellen können (z. B. beim Drucken eines Syntaxfehlers). Wenn der Compiler das Modul __test_reexports , generiert er ein neues Symbol für den Bezeichner. Obwohl die vom Compiler generierten __test_reexports möglicherweise denselben Namen wie Ihr generisches Modul haben, wird sein Symbol nicht verwendet. Diese Technik verhindert Namenskollisionen während der Codegenerierung und ist die Grundlage für die Hygiene des Rust-Makrosystems.

Schritt 2: Umreifen der Umreifung


Jetzt, da unsere Tests von der Wurzel unserer Kiste aus zugänglich sind, müssen wir etwas damit anfangen. libsyntax generiert ein solches Modul:

 pub mod __test { extern crate test; const TESTS: &'static [self::test::TestDescAndFn] = &[/*...*/]; #[main] pub fn main() { self::test::test_static_main(TESTS); } } 

Obwohl diese Konvertierung einfach ist, gibt sie uns viele Informationen darüber, wie die Tests tatsächlich durchgeführt werden. Tests werden in einem Array gesammelt und an den Testläufer mit dem Namen test_static_main . Wir werden zu TestDescAndFn , aber im Moment ist die wichtigste Schlussfolgerung, dass es eine Kiste namens test gibt , die Teil des Rust-Kernels ist und die gesamte Laufzeit zum Testen implementiert. Die test ist instabil, daher ist das Makro #[test] die einzige stabile Möglichkeit, mit ihr zu interagieren.

Schritt 3: Generieren eines Testobjekts


Wenn Sie zuvor Tests in Rust geschrieben haben, sind Sie möglicherweise mit einigen der optionalen Attribute vertraut, die für Testfunktionen verfügbar sind. Zum Beispiel kann ein Test mit #[should_panic] kommentiert werden, wenn wir erwarten, dass der Test eine Panik #[should_panic] . Es sieht ungefähr so ​​aus:

 #[test] #[should_panic] fn foo() { panic!("intentional"); } 

Dies bedeutet, dass unsere Tests mehr als einfache Funktionen sind und Konfigurationsinformationen enthalten. test codiert diese Konfigurationsdaten in eine Struktur namens TestDesc . Für jede Testfunktion in der Kiste analysiert libsyntax ihre Attribute und generiert eine Instanz von TestDesc . Anschließend werden TestDesc und die Testfunktion in der logischen Struktur TestDescAndFn , mit der test_static_main arbeitet. Für diesen Test sieht die generierte Instanz von TestDescAndFn aus:

 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())), } 

Sobald wir ein Array dieser Testobjekte erstellt haben, werden sie über die in Schritt 2 generierte Bindung an den Testläufer übergeben. Obwohl dieser Schritt als Teil des zweiten Schritts betrachtet werden kann, möchte ich als separates Konzept darauf aufmerksam machen, da dies der Schlüssel zur Implementierung eines benutzerdefinierten Tests ist Frameworks, aber dies wird ein weiterer Blog-Beitrag sein.

Nachwort: Forschungsmethoden


Obwohl ich viele Informationen direkt von den Compilerquellen erhalten habe, konnte ich herausfinden, dass es eine sehr einfache Möglichkeit gibt, zu sehen, was der Compiler tut. Der nächtliche Compiler-Build verfügt über ein instabiles Flag namens unpretty , mit dem Sie den Quellcode des Moduls nach dem Erweitern der Makros drucken können:

 $ rustc my_mod.rs -Z unpretty=hir 

Anmerkung des Übersetzers


Interessant ist, dass ich den Code des Testfalls nach der Offenlegung von Makros veranschaulichen werde:

Benutzerdefinierter Quellcode:

 #[test] fn my_test() { assert!(2+2 == 4); } fn main() {} 

Code nach dem Erweitern von Makros:

 #[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/de418095/


All Articles