2015-06-19 10 views
5

Ich versuche einen einfacheren Unit-Test-Runner für mein Rust-Projekt zu schreiben. Ich habe eine TestFixture-Eigenschaft erstellt, die von meinen Test-Fixture-Strukturen implementiert wird, ähnlich wie in anderen Test-Frameworks von der Unit-Test-Basisklasse. Das Merkmal ist ziemlich einfach. Dies ist meine PrüfvorrichtungWie kann ich eine Funktion an einen anderen Thread senden?

pub trait TestFixture { 
    fn setup(&mut self) ->() {} 
    fn teardown(&mut self) ->() {} 
    fn before_each(&mut self) ->() {} 
    fn after_each(&mut self) ->() {} 
    fn tests(&mut self) -> Vec<Box<Fn(&mut Self)>> 
     where Self: Sized { 
     Vec::new() 
    } 
} 

Funktion My Testlauf ist wie

folgt
pub fn test_fixture_runner<T: TestFixture>(fixture: &mut T) { 
    fixture.setup(); 

    let _r = fixture.tests().iter().map(|t| { 
     let handle = thread::spawn(move || { 
      fixture.before_each(); 
      t(fixture); 
      fixture.after_each(); 
     }); 

     if let Err(_) = handle.join() { 
      println!("Test failed!") 
     } 
    }); 

    fixture.teardown(); 
} 

ich den Fehler

src/tests.rs:73:22: 73:35 error: the trait `core::marker::Send` is not implemented for the type `T` [E0277] 
src/tests.rs:73   let handle = thread::spawn(move || { 
            ^~~~~~~~~~~~~ 
note: in expansion of closure expansion 
src/tests.rs:69:41: 84:6 note: expansion site 
src/tests.rs:73:22: 73:35 note: `T` cannot be sent between threads safely 
src/tests.rs:73   let handle = thread::spawn(move || { 
            ^~~~~~~~~~~~~ 
note: in expansion of closure expansion 
src/tests.rs:69:41: 84:6 note: expansion site 
src/tests.rs:73:22: 73:35 error: the trait `core::marker::Sync` is not implemented for the type `for<'r> core::ops::Fn(&'r mut T)` [E0277] 
src/tests.rs:73   let handle = thread::spawn(move || { 
            ^~~~~~~~~~~~~ 
note: in expansion of closure expansion 
src/tests.rs:69:41: 84:6 note: expansion site 
src/tests.rs:73:22: 73:35 note: `for<'r> core::ops::Fn(&'r mut T)` cannot be shared between threads safely 
src/tests.rs:73   let handle = thread::spawn(move || { 
            ^~~~~~~~~~~~~ 
note: in expansion of closure expansion 

Ich habe versucht, das Hinzufügen Arcs um die Typen auf den Thread gesendet werden , kein Würfel, der gleiche Fehler.

pub fn test_fixture_runner<T: TestFixture>(fixture: &mut T) { 
    fixture.setup(); 

    let fix_arc = Arc::new(Mutex::new(fixture)); 
    let _r = fixture.tests().iter().map(|t| { 
     let test_arc = Arc::new(Mutex::new(t)); 
     let fix_arc_clone = fix_arc.clone(); 
     let test_arc_clone = test_arc.clone(); 
     let handle = thread::spawn(move || { 
      let thread_test = test_arc_clone.lock().unwrap(); 
      let thread_fix = fix_arc_clone.lock().unwrap(); 
      (*thread_fix).before_each(); 
      (*thread_test)(*thread_fix); 
      (*thread_fix).after_each(); 
     }); 

     if let Err(_) = handle.join() { 
      println!("Test failed!") 
     } 
    }); 

    fixture.teardown(); 
} 

Eine Probe Prüfvorrichtung wäre so etwas wie

struct BuiltinTests { 
    pwd: PathBuf 
} 

impl TestFixture for BuiltinTests { 
    fn setup(&mut self) { 
     let mut pwd = env::temp_dir(); 
     pwd.push("pwd"); 

     fs::create_dir(&pwd); 
     self.pwd = pwd; 
    } 

    fn teardown(&mut self) { 
     fs::remove_dir(&self.pwd); 
    } 

    fn tests(&mut self) -> Vec<Box<Fn(&mut BuiltinTests)>> { 
     vec![Box::new(BuiltinTests::cd_with_no_args)] 
    } 
} 

impl BuiltinTests { 
    fn new() -> BuiltinTests { 
     BuiltinTests { 
      pwd: PathBuf::new() 
     } 
    } 
} 

fn cd_with_no_args(&mut self) { 
    let home = String::from("/"); 
    env::set_var("HOME", &home); 

    let mut cd = Cd::new(); 
    cd.run(&[]); 

    assert_eq!(env::var("PWD"), Ok(home)); 
} 

#[test] 
fn cd_tests() { 
    let mut builtin_tests = BuiltinTests::new(); 
    test_fixture_runner(&mut builtin_tests); 
} 

Meine ganze Absicht der Verwendung von Threads ist isoliert von den Testläufer sein. Wenn ein Test eine Behauptung nicht besteht, verursacht es eine Panik, die den Läufer tötet. Danke für irgendeinen Einblick, ich bin bereit, mein Design zu ändern, wenn das das Panikproblem beheben wird.

+1

Wenn Sie sich nicht für Threading interessieren, sondern nur das Panic-Capturen möchten, können Sie ['std :: thread :: catch_panic'] verwenden (https://doc.rust-lang.org/nightly/std /thread/fn.catch_panic.html) –

Antwort

7

Es gibt einige Probleme mit Ihrem Code, ich zeige Ihnen, wie Sie sie nacheinander beheben.

Das erste Problem besteht darin, dass Sie map() verwenden, um über einen Iterator zu iterieren. Es wird nicht korrekt funktionieren, weil map() faul ist - wenn Sie den Iterator nicht konsumieren, wird die Schließung, die Sie ihm übergeben haben, nicht ausgeführt. Der richtige Weg ist for Schleife zu verwenden:

for t in fixture().tests().iter() { 

Zweitens sind Sie den Vektor der Verschlüsse durch Bezugnahme Iterieren:

fixture.tests().iter().map(|t| { 

iter() auf einem Vec<T> gibt einen Iterator Elemente des Typs Nachgeben &T, so Ihre t wird vom Typ &Box<Fn(&mut Self)> sein. Box<Fn(&mut T)> implementiert jedoch Sync nicht standardmäßig (es handelt sich um ein Merkmalsobjekt, das keine Informationen zum zugrunde liegenden Typ enthält, außer dass Sie explizit angegeben haben). Daher kann &Box<Fn(&mut T)> nicht über mehrere Threads hinweg verwendet werden. Das ist der zweite Fehler, den Sie sehen.

Höchstwahrscheinlich möchten Sie diese Verschlüsse nicht als Referenz verwenden; Sie möchten sie wahrscheinlich vollständig in den erzeugten Thread verschieben. Dafür benötigen Sie into_iter() statt iter() zu verwenden:

for t in fixture.tests().into_iter() { 

Jetzt wird t von Box<Fn(&mut T)> Typ sein.Es kann jedoch immer noch nicht über Threads gesendet werden. Auch hier handelt es sich um ein Merkmalsobjekt, und der Compiler weiß nicht, ob der darin enthaltene Typ Send ist. Dafür benötigen Sie Send gebunden an die Art des Verschlusses hinzuzufügen:

fn tests(&mut self) -> Vec<Box<Fn(&mut Self)+Send>> 

Jetzt ist der Fehler über Fn verschwunden ist.

Der letzte Fehler ist Send wird nicht für T implementiert. Wir brauchen eine Send gebunden an T hinzuzufügen:

pub fn test_fixture_runner<T: TestFixture+Send>(fixture: &mut T) { 

Und nun der Fehler wird verständlicher:

test.rs:18:22: 18:35 error: captured variable `fixture` does not outlive the enclosing closure 
test.rs:18   let handle = thread::spawn(move || { 
           ^~~~~~~~~~~~~ 
note: in expansion of closure expansion 
test.rs:18:5: 28:6 note: expansion site 
test.rs:15:66: 31:2 note: captured variable is valid for the anonymous lifetime #1 defined on the block at 15:65 
test.rs:15 pub fn test_fixture_runner<T: TestFixture+Send>(fixture: &mut T) { 
test.rs:16  fixture.setup(); 
test.rs:17 
test.rs:18  for t in fixture.tests().into_iter() { 
test.rs:19   let handle = thread::spawn(move || { 
test.rs:20    fixture.before_each(); 
      ... 
note: closure is valid for the static lifetime 

Dieser Fehler tritt auf, weil Sie versuchen, einen Verweis in einem spawn() ed Thread zu verwenden. spawn() erfordert, dass sein Abschlussargument 'static gebunden hat, dh seine erfasste Umgebung darf keine Referenzen mit Nichtlebensdauer enthalten. Aber genau das passiert hier - &mut T ist nicht 'static. spawn() Design verbietet nicht, das Verbinden zu vermeiden, daher ist es ausdrücklich so geschrieben, dass nicht-'static Verweise auf den erzeugten Thread ausgeschlossen werden.

Beachten Sie, dass, während Sie &mut T verwenden, dieser Fehler unvermeidlich ist, auch wenn Sie &mut T in Arc setzen, weil dann die Lebensdauer der &mut T würde in Arc „gespeichert“ werden und so Arc<Mutex<&mut T>> wird auch nicht 'static sein.

Es gibt zwei Möglichkeiten zu tun, was Sie wollen.

Zuerst können Sie die instabile API thread::scoped() verwenden. Es ist instabil, weil it is shown Speicherunsicherheit in sicherem Code zuzulassen, und der Plan ist, eine Art Ersatz für es in der Zukunft bereitzustellen. Sie können jedoch in nächtlichen Rust verwenden (es wird kein Speicher unsafety selbst verursachen, nur in speziell gestalteten Situationen):

pub fn test_fixture_runner<T: TestFixture+Send>(fixture: &mut T) { 
    fixture.setup(); 

    let tests = fixture.lock().unwrap().tests(); 
    for t in tests.into_iter() { 
     let f = &mut *fixture; 

     let handle = thread::scoped(move || { 
      f.before_each(); 
      t(f); 
      f.after_each(); 
     }); 

     handle.join(); 
    } 

    fixture.teardown(); 
} 

Dieser Code kompiliert, weil scoped() so geschrieben ist, dass sie garantiert (in in den meisten Fällen), dass der Thread nicht alle erfassten Referenzen überlebt. Ich musste fixture reborrow da sonst (weil &mut Referenzen nicht kopierbar sind) würde es in den Thread verschoben werden und wäre verboten. Außerdem musste ich tests Variable extrahieren, weil sonst der Mutex für die Dauer der for-Schleife durch den Haupt-Thread gesperrt wird, was es natürlich nicht erlauben würde, ihn in den Kind-Threads zu sperren.

Mit scoped() können Sie jedoch die Panik im untergeordneten Thread nicht isolieren. Wenn der untergeordnete Thread in Panik gerät, wird diese Panik von join() Anruf erneut ausgelöst. Dies kann oder auch nicht ein Problem im Allgemeinen sein, aber ich denke, es ist ein Problem für Ihren Code.

Ein anderer Weg ist Ihr Code Refactoring das Gerät in Arc<Mutex<..>> von Anfang an zu halten:

pub fn test_fixture_runner<T: TestFixture + Send + 'static>(fixture: Arc<Mutex<T>>) { 
    fixture.lock().unwrap().setup(); 

    for t in fixture.lock().unwrap().tests().into_iter() { 
     let fixture = fixture.clone(); 

     let handle = thread::spawn(move || { 
      let mut fixture = fixture.lock().unwrap(); 

      fixture.before_each(); 
      t(&mut *fixture); 
      fixture.after_each(); 
     }); 

     if let Err(_) = handle.join() { 
      println!("Test failed!") 
     } 
    } 

    fixture.lock().unwrap().teardown(); 
} 

Beachten Sie, dass jetzt T auch 'static, wieder sein muss, weil es sonst nicht mit thread::spawn() verwendet werden könnten, wie es erfordert 'static. fixture innerhalb des inneren Verschlusses ist nicht &mut T sondern ein MutexGuard<T>, und so muss es explizit in &mut T umgewandelt werden, um es an t zu übergeben.

Dies mag übermäßig und unnötig komplex erscheinen, jedoch verhindert ein solches Design einer Programmiersprache viele Fehler in der Multithread-Programmierung. Jeder der obigen Fehler, die wir gesehen haben, ist gültig - jeder von ihnen wäre eine mögliche Ursache für Speicherunsicherheit oder Datenrennen, wenn er ignoriert würde.

+0

Danke, diese Lösung war sehr beschreibend und informativ. Es blockiert jedoch in der Zeile 'let mut fixture = fixture.lock(). Unwrap();' und wird nicht fortgesetzt. Weißt du, warum das wäre? –

+0

In der Tat, du hast Recht. Dies liegt daran, dass der temporäre Mutex-Schutz, der in dem For-Schleifen-Initialisierer (für t in fixture.lock() ...) erhalten wird, erst am Ende des for-Blocks freigegeben wird, was natürlich verhindert, dass die Sperre in den Kind-Threads erhalten wird . Die Lösung besteht darin, den Vektor getrennt zu extrahieren. Ich habe meine Antwort aktualisiert. –

0

Wie in den Rust HandBook's Concurrency section erklärt:

Wenn ein Typ T implementiert senden, es zeigt den Compiler, den etwas von dieser Art der Lage ist, das Eigentum sicher zwischen Threads übertragen zu haben.

Wenn Sie Send nicht implementieren, können die Eigentumsrechte nicht zwischen Threads übertragen werden.

+3

Dies ist ein ziemlich generischer Hinweis, und die meiste Zeit sollten * Sie * (der Entwickler) 'Send' nicht selbst implementieren: Der Compiler implementiert' Send', wenn er es für richtig hält. Ich denke, Sie sollten diese Antwort überprüfen und vielleicht ein wenig mehr auf das spezifische Bit des Codes, das das OP zeigte, zuschneiden. –

Verwandte Themen