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