Support asynchronous tests (#600)

* Tweak the implementation of heap closures

This commit updates the implementation of the `Closure` type to internally store
an `Rc` and be suitable for dropping a `Closure` during the execution of the
closure. This is currently needed for promises but may be generally useful as
well!

* Support asynchronous tests

This commit adds support for executing tests asynchronously. This is modeled
by tests returning a `Future` instead of simply executing inline, and is
signified with `#[wasm_bindgen_test(async)]`.

Support for this is added through a new `wasm-bindgen-futures` crate which is a
binding between the `futures` crate and JS `Promise` objects.

Lots more details can be found in the details of the commit, but one of the end
results is that the `web-sys` tests are now entirely contained in the same test
suite and don't need `npm install` to be run to execute them!

* Review tweaks

* Add some bindings for `Function.call` to `js_sys`

Name them `call0`, `call1`, `call2`, ... for the number of arguments being
passed.

* Use oneshots channels with `JsFuture`

It did indeed clean up the implementation!
This commit is contained in:
Alex Crichton
2018-08-01 15:52:24 -05:00
committed by GitHub
parent 4181afea45
commit eee71de0ce
34 changed files with 1167 additions and 333 deletions

View File

@ -7,10 +7,13 @@ license = "MIT/Apache-2.0"
repository = "https://github.com/rustwasm/wasm-bindgen"
[dependencies]
wasm-bindgen-test-macro = { path = '../test-macro', version = '=0.2.15' }
wasm-bindgen = { path = '../..', version = '0.2.15' }
js-sys = { path = '../js-sys', version = '0.2.0' }
console_error_panic_hook = '0.1'
futures = "0.1"
js-sys = { path = '../js-sys', version = '0.2.0' }
scoped-tls = "0.1"
wasm-bindgen = { path = '../..', version = '0.2.15' }
wasm-bindgen-futures = { path = '../futures', version = '0.2.15' }
wasm-bindgen-test-macro = { path = '../test-macro', version = '=0.2.15' }
[lib]
test = false

View File

@ -100,6 +100,24 @@ ton of documentation just yet, but a taste of how it works is:
And that's it! You've now got a test harness executing native wasm code inside
of Node.js and you can use `cargo test` as you normally would for workflows.
## Asynchronous Tests
Not all tests can execute immediately and some may need to do "blocking" work
like fetching resources and/or other bits and pieces. To accommodate this
asynchronous tests are also supported through the `futures` crate:
```rust
#[wasm_bindgen_test(async)]
fn my_test() -> impl Future<Item = (), Error = JsValue> {
// ...
}
```
The test will pass if the future resolves without panicking or returning an
error, and otherwise the test will fail.
This support is currently powered by the `wasm-bindgen-futures` crate.
## Components
The test harness is made of three separate components, but you typically don't
@ -165,8 +183,5 @@ Things that'd be awesome to support in the future:
* Arguments to `wasm-bindgen-test-runner` which are the same as `wasm-bindgen`,
for example `--debug` to affect the generated output.
* Built-in webserver to `wasm-bindgen-test-runner`. This would be handy for
running tests in a browser while developing.
* Headless browser testing to allow for testing in a browser on CI.
* Running each test in its own wasm instance to avoid poisoning the environment
on panic

View File

@ -0,0 +1,16 @@
[package]
name = "sample"
version = "0.1.0"
authors = ["The wasm-bindgen Authors"]
[lib]
test = false
[dependencies]
futures = "0.1"
js-sys = { path = '../../js-sys' }
wasm-bindgen = { path = '../../..' }
wasm-bindgen-futures = { path = '../../futures' }
[dev-dependencies]
wasm-bindgen-test = { path = '..' }

View File

@ -0,0 +1,4 @@
# Sample test crate
This is a dummy crate used to test changes to the `wasm-bindgen-test` crate,
this'll never be published nor tested on CI, it's intended for human inspection.

View File

@ -0,0 +1,64 @@
#![feature(use_extern_macros)]
#[macro_use]
extern crate futures;
extern crate js_sys;
extern crate wasm_bindgen;
extern crate wasm_bindgen_futures;
use std::time::Duration;
use futures::prelude::*;
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
pub struct Timeout {
id: u32,
inner: JsFuture,
}
#[wasm_bindgen]
extern {
#[wasm_bindgen(js_name = setTimeout)]
fn set_timeout(closure: JsValue, millis: f64) -> u32;
#[wasm_bindgen(js_name = clearTimeout)]
fn clear_timeout(id: u32);
}
impl Timeout {
pub fn new(dur: Duration) -> Timeout {
let millis = dur.as_secs()
.checked_mul(1000)
.unwrap()
.checked_add(dur.subsec_millis() as u64)
.unwrap() as f64; // TODO: checked cast
let mut id = None;
let promise = Promise::new(&mut |resolve, _reject| {
id = Some(set_timeout(resolve.into(), millis));
});
Timeout {
id: id.unwrap(),
inner: JsFuture::from(promise),
}
}
}
impl Future for Timeout {
type Item = ();
type Error = JsValue;
fn poll(&mut self) -> Poll<(), JsValue> {
let _obj = try_ready!(self.inner.poll());
Ok(().into())
}
}
impl Drop for Timeout {
fn drop(&mut self) {
clear_timeout(self.id);
}
}

View File

@ -0,0 +1,10 @@
#![feature(use_extern_macros)]
extern crate futures;
extern crate sample;
extern crate wasm_bindgen;
extern crate wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
pub mod common;

View File

@ -0,0 +1,45 @@
use std::time::Duration;
use futures::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*;
use sample::Timeout;
#[wasm_bindgen_test]
fn pass() {
console_log!("DO NOT SEE ME");
}
#[wasm_bindgen_test(async)]
fn pass_after_2s() -> impl Future<Item = (), Error = JsValue> {
console_log!("immediate log");
Timeout::new(Duration::new(1, 0))
.and_then(|()| {
console_log!("log after 1s");
Timeout::new(Duration::new(1, 0)).map(|()| {
console_log!("log at end");
})
})
}
#[wasm_bindgen_test]
fn fail() {
console_log!("helpful messsage, please see me");
panic!("this is a failing test");
}
#[wasm_bindgen_test(async)]
fn fail_after_3s() -> impl Future<Item = (), Error = JsValue> {
console_log!("immediate log");
Timeout::new(Duration::new(1, 0))
.and_then(|()| {
console_log!("log after 1s");
Timeout::new(Duration::new(1, 0)).and_then(|()| {
console_log!("log after 2s");
Timeout::new(Duration::new(1, 0)).map(|()| {
panic!("end");
})
})
})
}

View File

@ -0,0 +1,8 @@
#![feature(use_extern_macros)]
extern crate futures;
extern crate sample;
extern crate wasm_bindgen;
extern crate wasm_bindgen_test;
pub mod common;

View File

@ -5,10 +5,14 @@
#![feature(use_extern_macros)]
#![deny(missing_docs)]
extern crate wasm_bindgen_test_macro;
extern crate wasm_bindgen;
extern crate js_sys;
extern crate console_error_panic_hook;
extern crate futures;
extern crate js_sys;
#[macro_use]
extern crate scoped_tls;
extern crate wasm_bindgen;
extern crate wasm_bindgen_futures;
extern crate wasm_bindgen_test_macro;
pub use wasm_bindgen_test_macro::wasm_bindgen_test;

View File

@ -52,32 +52,14 @@ impl super::Formatter for Browser {
self.pre.set_inner_html(&html);
}
fn log_start(&self, name: &str) {
let data = format!("test {} ... ", name);
let mut html = self.pre.inner_html();
html.push_str(&data);
self.pre.set_inner_html(&html);
fn log_test(&self, name: &str, result: &Result<(), JsValue>) {
let s = if result.is_ok() { "ok" } else { "FAIL" };
self.writeln(&format!("test {} ... {}", name, s));
}
fn log_success(&self) {
let mut html = self.pre.inner_html();
html.push_str("ok\n");
self.pre.set_inner_html(&html);
}
fn log_ignored(&self) {
let mut html = self.pre.inner_html();
html.push_str("ignored\n");
self.pre.set_inner_html(&html);
}
fn log_failure(&self, err: JsValue) -> String {
let mut html = self.pre.inner_html();
html.push_str("FAIL\n");
self.pre.set_inner_html(&html);
fn stringify_error(&self, err: &JsValue) -> String {
// TODO: this should be a checked cast to `Error`
let err = Error::from(err);
let err = Error::from(err.clone());
let name = String::from(err.name());
let message = String::from(err.message());
let err = BrowserError::from(JsValue::from(err));

View File

@ -1,7 +1,7 @@
//! Runtime detection of whether we're in node.js or a browser.
use wasm_bindgen::prelude::*;
use js_sys::{Array, Function};
use js_sys::Function;
#[wasm_bindgen]
extern {
@ -54,7 +54,7 @@ pub fn is_browser() -> bool {
//
// Whew!
let this = Function::new_no_args("return this")
.apply(&JsValue::undefined(), &Array::new())
.call0(&JsValue::undefined())
.unwrap();
assert!(this != JsValue::undefined());
This::from(this).self_() != JsValue::undefined()

View File

@ -61,21 +61,21 @@
// This is used for test filters today.
//
// * The `Context::run` function is called. Again, the generated JS has gathered
// all wasm tests to be executed into a list, and it's passed in here. Again,
// it's very important that these functions are JS values, not function
// pointers in Rust.
// all wasm tests to be executed into a list, and it's passed in here.
//
// * Next, `Context::run` will proceed to execute all of the functions. When a
// function is executed we're invoking a JS function, which means we're
// allowed to catch exceptions. This is how we handle failing tests without
// aborting the entire process.
// * Next, `Context::run` returns a `Promise` representing the eventual
// execution of all the tests. The Rust `Future` that's returned will work
// with the tests to ensure that everything's executed by the time the
// `Promise` resolves.
//
// * When a test executes, it's executing an entry point generated by
// `#[wasm_bindgen_test]`. The test informs the `Context` of its name and
// other metadata, and then `Context::execute` actually invokes the tests
// itself (which currently is a unit function).
// other metadata, and then `Context::execute_*` function creates a future
// representing the execution of the test. This feeds back into the future
// returned by `Context::run` to finish the test suite.
//
// * Finally, after all tests are run, the `Context` prints out all the results.
// * Finally, after all tests are run, the `Context`'s future resolves, prints
// out all the result, and finishes in JS.
//
// ## Other various notes
//
@ -87,13 +87,25 @@
// Overall this is all somewhat in flux as it's pretty new, and feedback is
// always of course welcome!
use std::cell::{RefCell, Cell};
use std::fmt;
use std::mem;
use std::rc::Rc;
use console_error_panic_hook;
use js_sys::{Array, Function};
use futures::future;
use futures::prelude::*;
use js_sys::{Array, Function, Promise};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
// Maximum number of tests to execute concurrently. Eventually this should be a
// configuration option specified at runtime or at compile time rather than
// baked in here.
//
// Currently the default is 1 because the DOM has a lot of shared state, and
// conccurrently doing things by default would likely end up in a bad situation.
const CONCURRENCY: usize = 1;
pub mod node;
pub mod browser;
@ -105,15 +117,15 @@ pub mod detect;
/// drive test execution.
#[wasm_bindgen]
pub struct Context {
state: Rc<State>,
}
struct State {
/// An optional filter used to restrict which tests are actually executed
/// and which are ignored. This is passed via the `args` function which
/// comes from the command line of `wasm-bindgen-test-runner`. Currently
/// this is the only "CLI option"
filter: Option<String>,
/// The current test that is executing. If `None` no test is executing, if
/// `Some` it's the name of the tests.
current_test: RefCell<Option<String>>,
filter: RefCell<Option<String>>,
/// Counter of the number of tests that have succeeded.
succeeded: Cell<usize>,
@ -121,38 +133,59 @@ pub struct Context {
/// Counter of the number of tests that have been ignored
ignored: Cell<usize>,
/// A list of all tests which have failed. The first element of this pair is
/// the name of the test that failed, and the second is all logging
/// information (formatted) associated with the failure.
failures: RefCell<Vec<(String, String)>>,
/// A list of all tests which have failed.
///
/// Each test listed here is paired with a `JsValue` that represents the
/// exception thrown which caused the test to fail.
failures: RefCell<Vec<(Test, JsValue)>>,
/// Sink for `console.log` invocations when a test is running. This is
/// filled in by the `Context::console_log` function below while a test is
/// executing (aka while `current_test` above is `Some`).
current_log: RefCell<String>,
current_error: RefCell<String>,
/// Remaining tests to execute, when empty we're just waiting on the
/// `Running` tests to finish.
remaining: RefCell<Vec<Test>>,
/// Flag set as a test executes if it was actually ignored.
ignore_this_test: Cell<bool>,
/// List of currently executing tests. These tests all involve some level
/// of asynchronous work, so they're sitting on the running list.
running: RefCell<Vec<Test>>,
/// How to actually format output, either node.js or browser-specific
/// implementation.
formatter: Box<Formatter>,
}
/// Representation of one test that needs to be executed.
///
/// Tests are all represented as futures, and tests perform no work until their
/// future is polled.
struct Test {
name: String,
future: Box<Future<Item = (), Error = JsValue>>,
output: Rc<RefCell<Output>>,
}
/// Captured output of each test.
#[derive(Default)]
struct Output {
log: String,
error: String,
}
trait Formatter {
/// Writes a line of output, typically status information.
fn writeln(&self, line: &str);
fn log_start(&self, name: &str);
fn log_success(&self);
fn log_ignored(&self);
fn log_failure(&self, err: JsValue) -> String;
/// Log the result of a test, either passing or failing.
fn log_test(&self, name: &str, result: &Result<(), JsValue>);
/// Convert a thrown value into a string, using platform-specific apis
/// perhaps to turn the error into a string.
fn stringify_error(&self, val: &JsValue) -> String;
}
#[wasm_bindgen]
extern {
#[wasm_bindgen(js_namespace = console, js_name = log)]
#[doc(hidden)]
pub fn console_log(s: &str);
pub fn js_console_log(s: &str);
// General-purpose conversion into a `String`.
#[wasm_bindgen(js_name = String)]
@ -161,11 +194,10 @@ extern {
/// Internal implementation detail of the `console_log!` macro.
pub fn log(args: &fmt::Arguments) {
console_log(&args.to_string());
js_console_log(&args.to_string());
}
#[wasm_bindgen]
impl Context {
/// Creates a new context ready to run tests.
///
@ -181,15 +213,15 @@ impl Context {
None => Box::new(browser::Browser::new()),
};
Context {
filter: None,
current_test: RefCell::new(None),
succeeded: Cell::new(0),
ignored: Cell::new(0),
failures: RefCell::new(Vec::new()),
current_log: RefCell::new(String::new()),
current_error: RefCell::new(String::new()),
ignore_this_test: Cell::new(false),
formatter,
state: Rc::new(State {
filter: Default::default(),
failures: Default::default(),
ignored: Default::default(),
remaining: Default::default(),
running: Default::default(),
succeeded: Default::default(),
formatter,
}),
}
}
@ -204,107 +236,232 @@ impl Context {
// argument as a test filter.
//
// Everything else is rejected.
let mut filter = self.state.filter.borrow_mut();
for arg in args {
let arg = arg.as_string().unwrap();
if arg.starts_with("-") {
panic!("flag {} not supported", arg);
} else if self.filter.is_some() {
} else if filter.is_some() {
panic!("more than one filter argument cannot be passed");
}
self.filter = Some(arg);
*filter = Some(arg);
}
}
/// Executes a list of tests, returning whether any of them failed.
/// Executes a list of tests, returning a promise representing their
/// eventual completion.
///
/// This is the main entry point for executing tests. All the tests passed
/// in are the JS `Function` object that was plucked off the
/// `WebAssembly.Instance` exports list. This allows us to invoke it but
/// still catch JS exceptions.
pub fn run(&self, tests: Vec<JsValue>) -> bool {
let this = JsValue::null();
// Each entry point has one argument, a raw pointer to this `Context`,
// so build up that array we'll be passing all the functions.
let args = Array::new();
args.push(&JsValue::from(self as *const Context as u32));
/// `WebAssembly.Instance` exports list.
///
/// The promise returned resolves to either `true` if all tests passed or
/// `false` if at least one test failed.
pub fn run(&self, tests: Vec<JsValue>) -> Promise {
let noun = if tests.len() == 1 { "test" } else { "tests" };
self.formatter.writeln(&format!("running {} {}", tests.len(), noun));
self.formatter.writeln("");
self.state.formatter.writeln(&format!("running {} {}", tests.len(), noun));
self.state.formatter.writeln("");
// Execute all our test functions through their wasm shims (unclear how
// to pass native function pointers around here). Each test will
// execute one of the `execute_*` tests below which will push a
// future onto our `remaining` list, which we'll process later.
let cx_arg = (self as *const Context as u32).into();
for test in tests {
self.ignore_this_test.set(false);
// Use `Function.apply` to catch any exceptions and otherwise invoke
// the test.
let test = Function::from(test);
match test.apply(&this, &args) {
Ok(_) => {
if self.ignore_this_test.get() {
self.log_ignore()
} else {
self.log_success()
}
match Function::from(test).call1(&JsValue::null(), &cx_arg) {
Ok(_) => {}
Err(e) => {
panic!("exception thrown while creating a test: {}",
self.state.formatter.stringify_error(&e));
}
Err(e) => self.log_failure(e),
}
drop(self.current_test.borrow_mut().take());
*self.current_log.borrow_mut() = String::new();
*self.current_error.borrow_mut() = String::new();
}
self.log_results();
self.failures.borrow().len() == 0
// Now that we've collected all our tests we wrap everything up in a
// future to actually do all the processing, and pass it out to JS as a
// `Promise`.
let future = ExecuteTests(self.state.clone()).map(JsValue::from)
.map_err(|e| match e {});
future_to_promise(future)
}
}
scoped_thread_local!(static CURRENT_OUTPUT: RefCell<Output>);
/// Handler for `console.log` invocations.
///
/// If a test is currently running it takes the `args` array and stringifies
/// it and appends it to the current output of the test. Otherwise it passes
/// the arguments to the original `console.log` function, psased as
/// `original`.
//
// TODO: how worth is it to actually capture the output here? Due to the nature
// of futures/js we can't guarantee that all output is captured because JS code
// could just be executing in the void and we wouldn't know which test to
// attach it to. The main `test` crate in the rust repo also has issues about
// how not all output is captured, causing some inconsistencies sometimes.
#[wasm_bindgen]
pub fn __wbgtest_console_log(original: &Function, args: &Array) {
record(original, args, |output| &mut output.log)
}
/// Handler for `console.error` invocations.
///
/// Works the same as `console_log` above.
#[wasm_bindgen]
pub fn __wbgtest_console_error(original: &Function, args: &Array) {
record(original, args, |output| &mut output.error)
}
fn record(orig: &Function, args: &Array, dst: impl FnOnce(&mut Output) -> &mut String) {
if !CURRENT_OUTPUT.is_set() {
drop(orig.apply(&JsValue::null(), args));
return
}
fn log_start(&self, test: &str) {
let mut current_test = self.current_test.borrow_mut();
assert!(current_test.is_none());
*current_test = Some(test.to_string());
self.formatter.log_start(test);
CURRENT_OUTPUT.with(|output| {
let mut out = output.borrow_mut();
let dst = dst(&mut out);
args.for_each(&mut |val, idx, _array| {
if idx != 0 {
dst.push_str(" ");
}
dst.push_str(&stringify(&val));
});
dst.push_str("\n");
});
}
impl Context {
/// Entry point for a synchronous test in wasm. The `#[wasm_bindgen_test]`
/// macro generates invocations of this method.
pub fn execute_sync(&self, name: &str, f: impl FnOnce() + 'static) {
self.execute(name, future::lazy(|| Ok(f())));
}
fn log_success(&self) {
self.formatter.log_success();
self.succeeded.set(self.succeeded.get() + 1);
/// Entry point for an asynchronous in wasm. The
/// `#[wasm_bindgen_test(async)]` macro generates invocations of this
/// method.
pub fn execute_async<F>(&self, name: &str, f: impl FnOnce() -> F + 'static)
where F: Future<Item = (), Error = JsValue> + 'static,
{
self.execute(name, future::lazy(f))
}
fn log_ignore(&self) {
self.formatter.log_ignored();
self.ignored.set(self.ignored.get() + 1);
}
fn log_failure(&self, err: JsValue) {
let name = self.current_test.borrow().as_ref().unwrap().clone();
let log = mem::replace(&mut *self.current_log.borrow_mut(), String::new());
let error = mem::replace(&mut *self.current_error.borrow_mut(), String::new());
let mut msg = String::new();
if log.len() > 0 {
msg.push_str("log output:\n");
msg.push_str(&tab(&log));
msg.push_str("\n");
fn execute(
&self,
name: &str,
test: impl Future<Item = (), Error = JsValue> + 'static,
) {
// If our test is filtered out, record that it was filtered and move
// on, nothing to do here.
let filter = self.state.filter.borrow();
if let Some(filter) = &*filter {
if !name.contains(filter) {
let ignored = self.state.ignored.get();
self.state.ignored.set(ignored + 1);
return
}
}
if error.len() > 0 {
msg.push_str("error output:\n");
msg.push_str(&tab(&error));
msg.push_str("\n");
// Looks like we've got a test that needs to be executed! Push it onto
// the list of remaining tests.
let output = Rc::new(RefCell::new(Output::default()));
let future = TestFuture {
output: output.clone(),
test,
};
self.state.remaining.borrow_mut().push(Test {
name: name.to_string(),
future: Box::new(future),
output,
});
}
}
struct ExecuteTests(Rc<State>);
enum Never {}
impl Future for ExecuteTests {
type Item = bool;
type Error = Never;
fn poll(&mut self) -> Poll<bool, Never> {
let mut running = self.0.running.borrow_mut();
let mut remaining = self.0.remaining.borrow_mut();
// First up, try to make progress on all active tests. Remove any
// finished tests.
for i in (0..running.len()).rev() {
let result = match running[i].future.poll() {
Ok(Async::Ready(_jsavl)) => Ok(()),
Ok(Async::NotReady) => continue,
Err(e) => Err(e),
};
let test = running.remove(i);
self.0.log_test_result(test, result);
}
// Next up, try to schedule as many tests as we can. Once we get a test
// we `poll` it once to ensure we'll receive notifications. We only
// want to schedule up to a maximum amount of work though, so this may
// not schedule all tests.
while running.len() < CONCURRENCY {
let mut test = match remaining.pop() {
Some(test) => test,
None => break,
};
let result = match test.future.poll() {
Ok(Async::Ready(())) => Ok(()),
Ok(Async::NotReady) => {
running.push(test);
continue
}
Err(e) => Err(e),
};
self.0.log_test_result(test, result);
}
// Tests are still executing, we're registered to get a notification,
// keep going.
if running.len() != 0 {
return Ok(Async::NotReady)
}
// If there are no tests running then we must have finished everything,
// so we shouldn't have any more remaining tests either.
assert_eq!(remaining.len(), 0);
self.0.print_results();
let all_passed = self.0.failures.borrow().len() == 0;
Ok(Async::Ready(all_passed))
}
}
impl State {
fn log_test_result(&self, test: Test, result: Result<(), JsValue>) {
// Print out information about the test passing or failing
self.formatter.log_test(&test.name, &result);
// Save off the test for later processing when we print the final
// results.
match result {
Ok(()) => self.succeeded.set(self.succeeded.get() + 1),
Err(e) => self.failures.borrow_mut().push((test, e)),
}
msg.push_str("JS exception that was thrown:\n");
msg.push_str(&tab(&self.formatter.log_failure(err)));
self.failures.borrow_mut().push((name, msg));
}
fn log_results(&self) {
fn print_results(&self) {
let failures = self.failures.borrow();
if failures.len() > 0 {
self.formatter.writeln("\nfailures:\n");
for (test, logs) in failures.iter() {
let msg = format!("---- {} output ----\n{}", test, tab(logs));
self.formatter.writeln(&msg);
for (test, error) in failures.iter() {
self.print_failure(test, error);
}
self.formatter.writeln("failures:\n");
for (test, _) in failures.iter() {
self.formatter.writeln(&format!(" {}", test));
self.formatter.writeln(&format!(" {}", test.name));
}
}
self.formatter.writeln("");
@ -320,51 +477,77 @@ impl Context {
));
}
/// Handler for `console.log` invocations.
///
/// If a test is currently running it takes the `args` array and stringifies
/// it and appends it to the current output of the test. Otherwise it passes
/// the arguments to the original `console.log` function, psased as
/// `original`.
pub fn console_log(&self, original: &Function, args: &Array) {
self.log(original, args, &self.current_log)
}
/// Handler for `console.error` invocations.
///
/// Works the same as `console_log` above.
pub fn console_error(&self, original: &Function, args: &Array) {
self.log(original, args, &self.current_error)
}
fn log(&self, orig: &Function, args: &Array, dst: &RefCell<String>) {
if self.current_test.borrow().is_none() {
drop(orig.apply(&JsValue::null(), args));
return
fn print_failure(&self, test: &Test, error: &JsValue) {
let mut logs = String::new();
let output = test.output.borrow();
if output.log.len() > 0 {
logs.push_str("log output:\n");
logs.push_str(&tab(&output.log));
logs.push_str("\n");
}
let mut log = dst.borrow_mut();
args.for_each(&mut |val, idx, _array| {
if idx != 0 {
log.push_str(" ");
}
log.push_str(&stringify(&val));
});
log.push_str("\n");
if output.error.len() > 0 {
logs.push_str("error output:\n");
logs.push_str(&tab(&output.error));
logs.push_str("\n");
}
logs.push_str("JS exception that was thrown:\n");
let error_string = self.formatter.stringify_error(error);
logs.push_str(&tab(&error_string));
let msg = format!("---- {} output ----\n{}", test.name, tab(&logs));
self.formatter.writeln(&msg);
}
}
impl Context {
/// Entry point for a test in wasm. The `#[wasm_bindgen_test]` macro
/// generates invocations of this method.
pub fn execute(&self, name: &str, f: impl FnOnce()) {
self.log_start(name);
if let Some(filter) = &self.filter {
if !name.contains(filter) {
self.ignore_this_test.set(true);
return
}
}
f();
/// A wrapper future around each test
///
/// This future is what's actually executed for each test and is what's stored
/// inside of a `Test`. This wrapper future performs two critical functions:
///
/// * First, every time when polled, it configures the `CURRENT_OUTPUT` tls
/// variable to capture output for the current test. That way at least when
/// we've got Rust code running we'll be able to capture output.
///
/// * Next, this "catches panics". Right now all wasm code is configured as
/// panic=abort, but it's more like an exception in JS. It's pretty sketchy
/// to actually continue executing Rust code after an "abort", but we don't
/// have much of a choice for now.
///
/// Panics are caught here by using a shim function that is annotated with
/// `catch` so we can capture JS exceptions (which Rust panics become). This
/// way if any Rust code along the execution of a test panics we'll hopefully
/// capture it.
///
/// Note that both of the above aspects of this future are really just best
/// effort. This is all a bit of a hack right now when it comes down to it and
/// it definitely won't work in some situations. Hopefully as those situations
/// arise though we can handle them!
///
/// The good news is that everything should work flawlessly in the case where
/// tests have no output and execute successfully. And everyone always writes
/// perfect code on the first try, right? *sobs*
struct TestFuture<F> {
output: Rc<RefCell<Output>>,
test: F,
}
#[wasm_bindgen]
extern {
#[wasm_bindgen(catch)]
fn __wbg_test_invoke(f: &mut FnMut()) -> Result<(), JsValue>;
}
impl<F: Future<Error = JsValue>> Future for TestFuture<F> {
type Item = F::Item;
type Error = F::Error;
fn poll(&mut self) -> Poll<F::Item, F::Error> {
let test = &mut self.test;
let mut future_output = None;
CURRENT_OUTPUT.set(&self.output, || {
__wbg_test_invoke(&mut || future_output = Some(test.poll()))
})?;
future_output.unwrap()
}
}

View File

@ -4,22 +4,13 @@
//! for node itself.
use wasm_bindgen::prelude::*;
use js_sys::eval;
/// Implementation of the `Formatter` trait for node.js
pub struct Node {
/// Handle to node's imported `fs` module, imported dynamically because we
/// can't statically do it as it doesn't work in a browser.
fs: NodeFs,
}
#[wasm_bindgen]
extern {
// Type declarations for the `writeSync` function in node
type NodeFs;
#[wasm_bindgen(method, js_name = writeSync, structural)]
fn write_sync(this: &NodeFs, fd: i32, data: &[u8]);
// Not using `js_sys::Error` because node's errors specifically have a
// `stack` attribute.
type NodeError;
@ -34,35 +25,22 @@ impl Node {
if super::detect::is_browser() {
return None
}
// Use `eval` for now as a quick fix around static imports not working
// for dual browser/node support.
let import = eval("require(\"fs\")").unwrap();
Some(Node { fs: import.into() })
Some(Node { })
}
}
impl super::Formatter for Node {
fn writeln(&self, line: &str) {
super::console_log(line);
super::js_console_log(line);
}
fn log_start(&self, name: &str) {
let data = format!("test {} ... ", name);
self.fs.write_sync(2, data.as_bytes());
fn log_test(&self, name: &str, result: &Result<(), JsValue>) {
let s = if result.is_ok() { "ok" } else { "FAIL" };
self.writeln(&format!("test {} ... {}", name, s));
}
fn log_success(&self) {
self.fs.write_sync(2, b"ok\n");
}
fn log_ignored(&self) {
self.fs.write_sync(2, b"ignored\n");
}
fn log_failure(&self, err: JsValue) -> String {
self.fs.write_sync(2, b"ignored\n");
fn stringify_error(&self, err: &JsValue) -> String {
// TODO: should do a checked cast to `NodeError`
NodeError::from(err).stack()
NodeError::from(err.clone()).stack()
}
}