diff --git a/.travis.yml b/.travis.yml index 01326f24..78d2caf7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -78,10 +78,8 @@ matrix: install: - *INSTALL_NODE_VIA_NVM - *INSTALL_GECKODRIVER - - npm ci --verbose script: - - cargo test --manifest-path crates/web-sys/Cargo.toml - - cargo test --manifest-path crates/web-sys/Cargo.toml --target wasm32-unknown-unknown + - cargo test -p web-sys --target wasm32-unknown-unknown addons: firefox: latest if: branch = master @@ -93,7 +91,6 @@ matrix: - *INSTALL_NODE_VIA_NVM - *INSTALL_GECKODRIVER script: - - cargo test -p js-sys - cargo test -p js-sys --target wasm32-unknown-unknown addons: firefox: latest @@ -168,7 +165,7 @@ matrix: - cargo install-update -a script: - (cd guide && mdbook build) - - cargo doc --no-deps -p wasm-bindgen -p web-sys -p js-sys + - cargo doc --no-deps -p wasm-bindgen -p web-sys -p js-sys -p wasm-bindgen-futures - mv target/doc guide/book/api deploy: provider: pages diff --git a/Cargo.toml b/Cargo.toml index 1fab54cb..26b519e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ "crates/cli", "crates/js-sys", "crates/test", + "crates/test/sample", "crates/typescript", "crates/web-sys", "crates/webidl", diff --git a/crates/cli-support/src/js/rust2js.rs b/crates/cli-support/src/js/rust2js.rs index 965b685a..38386e43 100644 --- a/crates/cli-support/src/js/rust2js.rs +++ b/crates/cli-support/src/js/rust2js.rs @@ -214,7 +214,6 @@ impl<'a, 'b> Rust2Js<'a, 'b> { builder.rust_argument("this.a"); } builder - .rust_argument("this.b") .process(&closure.function)? .finish("function", "this.f") }; @@ -225,18 +224,16 @@ impl<'a, 'b> Rust2Js<'a, 'b> { let reset_idx = format!( "\ let cb{0} = {js};\n\ + cb{0}.f = wasm.__wbg_function_table.get(getGlobalArgument({f}));\n\ cb{0}.a = getGlobalArgument({a});\n\ - cb{0}.b = getGlobalArgument({b});\n\ - cb{0}.f = wasm.__wbg_function_table.get(getGlobalArgument({c}));\n\ let real = cb{0}.bind(cb{0});\n\ real.original = cb{0};\n\ idx{0} = getUint32Memory()[{0} / 4] = addHeapObject(real);\n\ ", abi, js = js, + f = self.global_idx(), a = self.global_idx(), - b = self.global_idx(), - c = self.global_idx(), ); self.prelude(&format!( "\ diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html b/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html index 566bcf70..c7eed7bb 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html @@ -20,17 +20,18 @@ } }; console.log = function() { - if (window.global_cx) - window.global_cx.console_log(orig_console_log, arguments); + if (window.console_log_redirect) + window.console_log_redirect(orig_console_log, arguments); else orig_console_log.apply(this, arguments); }; console.error = function() { - if (window.global_cx) - window.global_cx.console_error(orig_console_error, arguments); + if (window.console_error_redirect) + window.console_error_redirect(orig_console_error, arguments); else orig_console_error.apply(this, arguments); }; + window.__wbg_test_invoke = f => f(); diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/index.html b/crates/cli/src/bin/wasm-bindgen-test-runner/index.html index 88fad944..57a3dac7 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/index.html +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/index.html @@ -8,17 +8,19 @@ const orig_console_log = console.log; const orig_console_error = console.error; console.log = function() { - if (window.global_cx) - window.global_cx.console_log(orig_console_log, arguments); + if (window.console_log_redirect) + window.console_log_redirect(orig_console_log, arguments); else orig_console_log.apply(this, arguments); }; console.error = function() { - if (window.global_cx) - window.global_cx.console_error(orig_console_error, arguments); + if (window.console_error_redirect) + window.console_error_redirect(orig_console_error, arguments); else orig_console_error.apply(this, arguments); }; + + window.__wbg_test_invoke = f => f(); diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs index 48c8dfe0..cd0b3ce5 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs @@ -12,33 +12,38 @@ pub fn execute(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String]) let mut js_to_execute = format!(r#" const {{ exit }} = require('process'); - let cx = null; + let console_log_redirect = null; + let console_error_redirect = null; // override `console.log` and `console.error` before we import tests to // ensure they're bound correctly in wasm. This'll allow us to intercept // all these calls and capture the output of tests const prev_log = console.log; console.log = function() {{ - if (cx === null) {{ + if (console_log_redirect === null) {{ prev_log.apply(null, arguments); }} else {{ - cx.console_log(prev_log, arguments); + console_log_redirect(prev_log, arguments); }} }}; const prev_error = console.error; console.error = function() {{ - if (cx === null) {{ + if (console_error_redirect === null) {{ prev_error.apply(null, arguments); }} else {{ - cx.console_error(prev_error, arguments); + console_error_redirect(prev_error, arguments); }} }}; - function main(tests) {{ + global.__wbg_test_invoke = f => f(); + + async function main(tests) {{ const support = require("./{0}"); const wasm = require("./{0}_bg"); cx = new support.Context(); + console_log_redirect = support.__wbgtest_console_log; + console_error_redirect = support.__wbgtest_console_error; // Forward runtime arguments. These arguments are also arguments to the // `wasm-bindgen-test-runner` which forwards them to node which we @@ -46,7 +51,8 @@ pub fn execute(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String]) // filters for now. cx.args(process.argv.slice(2)); - if (!cx.run(tests.map(n => wasm[n]))) + const ok = await cx.run(tests.map(n => wasm[n])); + if (!ok) exit(1); }} @@ -64,6 +70,10 @@ pub fn execute(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String]) // And as a final addendum, exit with a nonzero code if any tests fail. js_to_execute.push_str(" main(tests) + .catch(e => { + console.error(e); + exit(1); + }); "); let js_path = tmpdir.join("run.js"); diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs index e793b0a6..1387bb3d 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs @@ -16,7 +16,7 @@ pub fn spawn( tests: &[String], ) -> Result Response + Send + Sync>, Error> { let mut js_to_execute = format!(r#" - import {{ Context }} from './{0}'; + import {{ Context, __wbgtest_console_log, __wbgtest_console_error }} from './{0}'; import * as wasm from './{0}_bg'; // Now that we've gotten to the point where JS is executing, update our @@ -30,7 +30,8 @@ pub fn spawn( await wasm.booted; const cx = Context.new(); - window.global_cx = cx; + window.console_log_redirect = __wbgtest_console_log; + window.console_error_redirect = __wbgtest_console_error; // Forward runtime arguments. These arguments are also arguments to the // `wasm-bindgen-test-runner` which forwards them to node which we @@ -38,7 +39,7 @@ pub fn spawn( // filters for now. cx.args({1:?}); - cx.run(test.map(s => wasm[s])); + await cx.run(test.map(s => wasm[s])); }} const tests = []; diff --git a/crates/futures/Cargo.toml b/crates/futures/Cargo.toml new file mode 100644 index 00000000..bffa5588 --- /dev/null +++ b/crates/futures/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "wasm-bindgen-futures" +version = "0.2.15" +authors = ["The wasm-bindgen Developers"] + +[dependencies] +futures = "0.1.20" +js-sys = { path = "../js-sys", version = '0.2.0' } +wasm-bindgen = { path = "../..", version = '0.2.15' } diff --git a/crates/futures/README.md b/crates/futures/README.md new file mode 100644 index 00000000..7e90bf29 --- /dev/null +++ b/crates/futures/README.md @@ -0,0 +1,12 @@ +# wasm-bindgen-futures + +[Documention][documentation] + +This is an experimental crate (aka just written) which is targeted at bridging +a Rust `Future` and a JS `Promise`. Internally it contains two conversions, one +from a JS `Promise` to a Rust `Future`, and another from a Rust `Future` to a +JS `Promise`. + +See the [documentation] for more info. + +[documentation]: https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen_futures/ diff --git a/crates/futures/src/lib.rs b/crates/futures/src/lib.rs new file mode 100644 index 00000000..60ab9ef5 --- /dev/null +++ b/crates/futures/src/lib.rs @@ -0,0 +1,278 @@ +//! A JS `Promise` to Rust `Future` bridge +//! +//! This crate provides a bridge for working with JS `Promise` types as a Rust +//! `Future`, and similarly contains utilities to turn a rust `Future` into a JS +//! `Promise`. This can be useful when working with asynchronous or otherwise +//! blocking work in Rust (wasm), and provides the ability to interoperate with +//! JS events and JS I/O primitives. +//! +//! There are two main interfaces in this crate currently: +//! +//! * `JsFuture` - a type that is constructed with a `Promise` and can then be +//! used as a `Future`. This Rust future will +//! resolve or reject with the value coming out of the `Promise`. +//! * `future_to_promise` - converts a Rust `Future` into a JS `Promise`. The future's result will translate to +//! either a rejected or resolved `Promise` in JS. +//! +//! These two types should provide enough of a bridge to interoperate the two +//! systems and make sure that Rust/JS can work together with asynchronous and +//! I/O work. + +#![deny(missing_docs)] +#![feature(use_extern_macros)] + +extern crate futures; +extern crate wasm_bindgen; +extern crate js_sys; + +use std::sync::Arc; +use std::cell::{RefCell, Cell}; + +use futures::executor::{self, Spawn, Notify}; +use futures::prelude::*; +use futures::sync::oneshot; +use js_sys::{Function, Promise}; +use wasm_bindgen::prelude::*; + +/// A Rust `Future` backed by a JS `Promise`. +/// +/// This type is constructed with a JS `Promise` object and translates it to a +/// Rust `Future`. This type implements the `Future` trait from the `futures` +/// crate and will either succeed or fail depending on what happens with the JS +/// `Promise`. +/// +/// Currently this type is constructed with `JsFuture::from`. +pub struct JsFuture { + resolved: oneshot::Receiver, + rejected: oneshot::Receiver, + callbacks: Option<(Closure, Closure)>, +} + +impl From for JsFuture { + fn from(js: Promise) -> JsFuture { + // Use the `then` method to schedule two callbacks, one for the + // resolved value and one for the rejected value. These two callbacks + // will be connected to oneshot channels which feed back into our + // future. + // + // This may not be the speediest option today but it should work! + let (tx1, rx1) = oneshot::channel(); + let (tx2, rx2) = oneshot::channel(); + let mut tx1 = Some(tx1); + let resolve = Closure::wrap(Box::new(move |val| { + drop(tx1.take().unwrap().send(val)); + }) as Box); + let mut tx2 = Some(tx2); + let reject = Closure::wrap(Box::new(move |val| { + drop(tx2.take().unwrap().send(val)); + }) as Box); + + js.then2(&resolve, &reject); + + JsFuture { + resolved: rx1, + rejected: rx2, + callbacks: Some((resolve, reject)), + } + } +} + +impl Future for JsFuture { + type Item = JsValue; + type Error = JsValue; + + fn poll(&mut self) -> Poll { + // Test if either our resolved or rejected side is finished yet. Note + // that they will return errors if they're disconnected which can't + // happen until we drop the `callbacks` field, which doesn't happen + // till we're done, so we dont need to handle that. + if let Ok(Async::Ready(val)) = self.resolved.poll() { + drop(self.callbacks.take()); + return Ok(val.into()) + } + if let Ok(Async::Ready(val)) = self.rejected.poll() { + drop(self.callbacks.take()); + return Err(val) + } + Ok(Async::NotReady) + } +} + +/// Converts a Rust `Future` into a JS `Promise`. +/// +/// This function will take any future in Rust and schedule it to be executed, +/// returning a JS `Promise` which can then be passed back to JS to get plumbed +/// into the rest of a system. +/// +/// The `future` provided must adhere to `'static` because it'll be scheduled +/// to run in the background and cannot contain any stack references. The +/// returned `Promise` will be resolved or rejected when the future completes, +/// depending on whether it finishes with `Ok` or `Err`. +/// +/// # Panics +/// +/// Note that in wasm panics are currently translated to aborts, but "abort" in +/// this case means that a JS exception is thrown. The wasm module is still +/// usable (likely erroneously) after Rust panics. +/// +/// If the `future` provided panics then the returned `Promise` **will not +/// resolve**. Instead it will be a leaked promise. This is an unfortunate +/// limitation of wasm currently that's hoped to be fixed one day! +pub fn future_to_promise(future: F) -> Promise + where F: Future + 'static, +{ + _future_to_promise(Box::new(future)) +} + +// Implementation of actually transforming a future into a JS `Promise`. +// +// The only primitive we have to work with here is `Promise::new`, which gives +// us two callbacks that we can use to either reject or resolve the promise. +// It's our job to ensure that one of those callbacks is called at the +// appropriate time. +// +// Now we know that JS (in general) can't block and is largely +// notification/callback driven. That means that our future must either have +// synchronous computational work to do, or it's "scheduled a notification" to +// happen. These notifications are likely callbacks to get executed when things +// finish (like a different promise or something like `setTimeout`). The general +// idea here is thus to do as much synchronous work as we can and then otherwise +// translate notifications of a future's task into "let's poll the future!" +// +// This isn't necessarily the greatest future executor in the world, but it +// should get the job done for now hopefully. +fn _future_to_promise(future: Box>) -> Promise { + let mut future = Some(executor::spawn(future)); + return Promise::new(&mut |resolve, reject| { + Package::poll(&Arc::new(Package { + spawn: RefCell::new(future.take().unwrap()), + resolve, + reject, + notified: Cell::new(State::Notified), + })); + }); + + struct Package { + // Our "spawned future". This'll have everything we need to poll the + // future and continue to move it forward. + spawn: RefCell>>>, + + // The current state of this future, expressed in an enum below. This + // indicates whether we're currently polling the future, received a + // notification and need to keep polling, or if we're waiting for a + // notification to come in (and no one is polling). + notified: Cell, + + // Our two callbacks connected to the `Promise` that we returned to JS. + // We'll be invoking one of these at the end. + resolve: Function, + reject: Function, + } + + // The possible states our `Package` (future) can be in, tracked internally + // and used to guide what happens when polling a future. + enum State { + // This future is currently and actively being polled. Attempting to + // access the future will result in a runtime panic and is considered a + // bug. + Polling, + + // This future has been notified, while it was being polled. This marker + // is used in the `Notify` implementation below, and indicates that a + // notification was received that the future is ready to make progress. + // If seen, however, it probably means that the future is also currently + // being polled. + Notified, + + // The future is blocked, waiting for something to happen. Stored here + // is a self-reference to the future itself so we can pull it out in + // `Notify` and continue polling. + // + // Note that the self-reference here is an Arc-cycle that will leak + // memory unless the future completes, but currently that should be ok + // as we'll have to stick around anyway while the future is executing! + // + // This state is removed as soon as a notification comes in, so the leak + // should only be "temporary" + Waiting(Arc), + } + + // No shared memory right now, wasm is single threaded, no need to worry + // about this! + unsafe impl Send for Package {} + unsafe impl Sync for Package {} + + impl Package { + // Move the future contained in `me` as far forward as we can. This will + // do as much synchronous work as possible to complete the future, + // ensuring that when it blocks we're scheduled to get notified via some + // callback somewhere at some point (vague, right?) + // + // TODO: this probably shouldn't do as much synchronous work as possible + // as it can starve other computations. Rather it should instead + // yield every so often with something like `setTimeout` with the + // timeout set to zero. + fn poll(me: &Arc) { + loop { + match me.notified.replace(State::Polling) { + // We received a notification while previously polling, or + // this is the initial poll. We've got work to do below! + State::Notified => {} + + // We've gone through this loop once and no notification was + // received while we were executing work. That means we got + // `NotReady` below and we're scheduled to receive a + // notification. Block ourselves and wait for later. + // + // When the notification comes in it'll notify our task, see + // our `Waiting` state, and resume the polling process + State::Polling => { + me.notified.set(State::Waiting(me.clone())); + break + } + + State::Waiting(_) => panic!("shouldn't see waiting state!"), + } + + let (val, f) = match me.spawn.borrow_mut().poll_future_notify(me, 0) { + // If the future is ready, immediately call the + // resolve/reject callback and then return as we're done. + Ok(Async::Ready(value)) => (value, &me.resolve), + Err(value) => (value, &me.reject), + + // Otherwise keep going in our loop, if we weren't notified + // we'll break out and start waiting. + Ok(Async::NotReady) => continue, + }; + + drop(f.call1(&JsValue::undefined(), &val)); + break + } + } + } + + impl Notify for Package { + fn notify(&self, _id: usize) { + match self.notified.replace(State::Notified) { + // we need to schedule polling to resume, so we do so + // immediately for now + State::Waiting(me) => Package::poll(&me), + + // we were already notified, and were just notified again; + // having now coalesced the notifications we return as it's + // still someone else's job to process this + State::Notified => {} + + // the future was previously being polled, and we've just + // switched it to the "you're notified" state. We don't have + // access to the future as it's being polled, so the future + // polling process later sees this notification and will + // continue polling. For us, though, there's nothing else to do, + // so we bail out. + // later see + State::Polling => {} + } + } + } +} diff --git a/crates/js-sys/src/lib.rs b/crates/js-sys/src/lib.rs index 5af6e4c8..b4c4198b 100644 --- a/crates/js-sys/src/lib.rs +++ b/crates/js-sys/src/lib.rs @@ -740,6 +740,34 @@ extern "C" { #[wasm_bindgen(method, catch)] pub fn apply(this: &Function, context: &JsValue, args: &Array) -> Result; + /// The `call()` method calls a function with a given this value and + /// arguments provided individually. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call + #[wasm_bindgen(method, catch, js_name = call)] + pub fn call0(this: &Function, context: &JsValue) -> Result; + + /// The `call()` method calls a function with a given this value and + /// arguments provided individually. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call + #[wasm_bindgen(method, catch, js_name = call)] + pub fn call1(this: &Function, context: &JsValue, arg1: &JsValue) -> Result; + + /// The `call()` method calls a function with a given this value and + /// arguments provided individually. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call + #[wasm_bindgen(method, catch, js_name = call)] + pub fn call2(this: &Function, context: &JsValue, arg1: &JsValue, arg2: &JsValue) -> Result; + + /// The `call()` method calls a function with a given this value and + /// arguments provided individually. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call + #[wasm_bindgen(method, catch, js_name = call)] + pub fn call3(this: &Function, context: &JsValue, arg1: &JsValue, arg2: &JsValue, arg3: &JsValue) -> Result; + /// The bind() method creates a new function that, when called, has its this keyword set to the provided value, /// with a given sequence of arguments preceding any provided when the new function is called. /// @@ -3009,3 +3037,101 @@ extern "C" { #[wasm_bindgen(static_method_of = Intl, js_name = getCanonicalLocales)] pub fn get_canonical_locales(s: &JsValue) -> Array; } + +// Promise +#[wasm_bindgen] +extern { + /// The `Promise` object represents the eventual completion (or failure) of + /// an asynchronous operation, and its resulting value. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise + pub type Promise; + + /// Creates a new `Promise` with the provided executor `cb` + /// + /// The `cb` is a function that is passed with the arguments `resolve` and + /// `reject`. The `cb` function is executed immediately by the `Promise` + /// implementation, passing `resolve` and `reject` functions (the executor + /// is called before the `Promise` constructor even returns the created + /// object). The `resolve` and `reject` functions, when called, resolve or + /// reject the promise, respectively. The executor normally initiates + /// some asynchronous work, and then, once that completes, either calls + /// the `resolve` function to resolve the promise or else rejects it if an + /// error occurred. + /// + /// If an error is thrown in the executor function, the promise is rejected. + /// The return value of the executor is ignored. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise + #[wasm_bindgen(constructor)] + pub fn new(cb: &mut FnMut(Function, Function)) -> Promise; + + /// The `Promise.all(iterable)` method returns a single `Promise` that + /// resolves when all of the promises in the iterable argument have resolved + /// or when the iterable argument contains no promises. It rejects with the + /// reason of the first promise that rejects. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all + #[wasm_bindgen(static_method_of = Promise)] + pub fn all(obj: JsValue) -> Promise; + + /// The `Promise.race(iterable)` method returns a promise that resolves or + /// rejects as soon as one of the promises in the iterable resolves or + /// rejects, with the value or reason from that promise. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race + #[wasm_bindgen(static_method_of = Promise)] + pub fn race(obj: JsValue) -> Promise; + + /// The `Promise.reject(reason)` method returns a `Promise` object that is + /// rejected with the given reason. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject + #[wasm_bindgen(static_method_of = Promise)] + pub fn reject(obj: JsValue) -> Promise; + + /// The `Promise.resolve(value)` method returns a `Promise` object that is + /// resolved with the given value. If the value is a promise, that promise + /// is returned; if the value is a thenable (i.e. has a "then" method), the + /// returned promise will "follow" that thenable, adopting its eventual + /// state; otherwise the returned promise will be fulfilled with the value. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve + #[wasm_bindgen(static_method_of = Promise)] + pub fn resolve(obj: JsValue) -> Promise; + + /// The `catch()` method returns a `Promise` and deals with rejected cases + /// only. It behaves the same as calling `Promise.prototype.then(undefined, + /// onRejected)` (in fact, calling `obj.catch(onRejected)` internally calls + /// `obj.then(undefined, onRejected)`). + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch + #[wasm_bindgen(method)] + pub fn catch(this: &Promise, cb: &Closure) -> Promise; + + /// The `then()` method returns a `Promise`. It takes up to two arguments: + /// callback functions for the success and failure cases of the `Promise`. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + #[wasm_bindgen(method)] + pub fn then(this: &Promise, cb: &Closure) -> Promise; + + /// Same as `then`, only with both arguments provided. + #[wasm_bindgen(method, js_name = then)] + pub fn then2(this: &Promise, + resolve: &Closure, + reject: &Closure) -> Promise; + + /// The `finally()` method returns a `Promise`. When the promise is settled, + /// whether fulfilled or rejected, the specified callback function is + /// executed. This provides a way for code that must be executed once the + /// `Promise` has been dealt with to be run whether the promise was + /// fulfilled successfully or rejected. + /// + /// This lets you avoid duplicating code in both the promise's `then()` and + /// `catch()` handlers. + /// + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally + #[wasm_bindgen(method)] + pub fn finally(this: &Promise, cb: &Closure) -> Promise; +} diff --git a/crates/js-sys/tests/wasm/RegExp.rs b/crates/js-sys/tests/wasm/RegExp.rs index 8af4946a..a9b60c70 100644 --- a/crates/js-sys/tests/wasm/RegExp.rs +++ b/crates/js-sys/tests/wasm/RegExp.rs @@ -1,4 +1,3 @@ -use wasm_bindgen::JsValue; use wasm_bindgen_test::*; use js_sys::*; diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 81846be9..c7b7a69f 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -3,7 +3,7 @@ #[macro_use] extern crate serde_derive; -pub const SCHEMA_VERSION: &str = "6"; +pub const SCHEMA_VERSION: &str = "7"; #[derive(Deserialize)] pub struct ProgramOnlySchema { diff --git a/crates/test-macro/src/lib.rs b/crates/test-macro/src/lib.rs index 486f34a7..4d7aa083 100644 --- a/crates/test-macro/src/lib.rs +++ b/crates/test-macro/src/lib.rs @@ -16,8 +16,18 @@ pub fn wasm_bindgen_test( attr: proc_macro::TokenStream, body: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - if attr.into_iter().next().is_some() { - panic!("this attribute currently takes no arguments"); + let mut attr = attr.into_iter(); + let mut async = false; + while let Some(token) = attr.next() { + match &token { + proc_macro::TokenTree::Ident(i) if i.to_string() == "async" => async = true, + _ => panic!("malformed `#[wasm_bindgen_test]` attribute"), + } + match &attr.next() { + Some(proc_macro::TokenTree::Punct(op)) if op.as_char() == ',' => {} + Some(_) => panic!("malformed `#[wasm_bindgen_test]` attribute"), + None => break, + } } let mut body = TokenStream::from(body).into_iter(); @@ -32,6 +42,12 @@ pub fn wasm_bindgen_test( let mut tokens = Vec::::new(); + let test_body = if async { + quote! { cx.execute_async(test_name, #ident); } + } else { + quote! { cx.execute_sync(test_name, #ident); } + }; + // We generate a `#[no_mangle]` with a known prefix so the test harness can // later slurp up all of these functions and pass them as arguments to the // main test harness. This is the entry point for all tests. @@ -41,7 +57,9 @@ pub fn wasm_bindgen_test( #[no_mangle] pub extern fn #name(cx: *const ::wasm_bindgen_test::__rt::Context) { unsafe { - (*cx).execute(concat!(module_path!(), "::", stringify!(#ident)), #ident); + let cx = &*cx; + let test_name = concat!(module_path!(), "::", stringify!(#ident)); + #test_body } } }).into_iter()); diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 335bf4bd..7d934151 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -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 diff --git a/crates/test/README.md b/crates/test/README.md index 69699cd1..442e89c0 100644 --- a/crates/test/README.md +++ b/crates/test/README.md @@ -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 { + // ... +} +``` + +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 diff --git a/crates/test/sample/Cargo.toml b/crates/test/sample/Cargo.toml new file mode 100644 index 00000000..591e147e --- /dev/null +++ b/crates/test/sample/Cargo.toml @@ -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 = '..' } diff --git a/crates/test/sample/README.md b/crates/test/sample/README.md new file mode 100644 index 00000000..f95bdf7c --- /dev/null +++ b/crates/test/sample/README.md @@ -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. diff --git a/crates/test/sample/src/lib.rs b/crates/test/sample/src/lib.rs new file mode 100644 index 00000000..b45cedf8 --- /dev/null +++ b/crates/test/sample/src/lib.rs @@ -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); + } +} diff --git a/crates/test/sample/tests/browser.rs b/crates/test/sample/tests/browser.rs new file mode 100644 index 00000000..5d5b36f8 --- /dev/null +++ b/crates/test/sample/tests/browser.rs @@ -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; diff --git a/crates/test/sample/tests/common/mod.rs b/crates/test/sample/tests/common/mod.rs new file mode 100644 index 00000000..f8679a8c --- /dev/null +++ b/crates/test/sample/tests/common/mod.rs @@ -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 { + 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 { + 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"); + }) + }) + }) +} + diff --git a/crates/test/sample/tests/node.rs b/crates/test/sample/tests/node.rs new file mode 100644 index 00000000..84d87170 --- /dev/null +++ b/crates/test/sample/tests/node.rs @@ -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; diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index bc349575..99326b17 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -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; diff --git a/crates/test/src/rt/browser.rs b/crates/test/src/rt/browser.rs index 6286e4fc..2fab7e22 100644 --- a/crates/test/src/rt/browser.rs +++ b/crates/test/src/rt/browser.rs @@ -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)); diff --git a/crates/test/src/rt/detect.rs b/crates/test/src/rt/detect.rs index c29f3724..b8ce683a 100644 --- a/crates/test/src/rt/detect.rs +++ b/crates/test/src/rt/detect.rs @@ -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() diff --git a/crates/test/src/rt/mod.rs b/crates/test/src/rt/mod.rs index 68ba6e12..83e80fac 100644 --- a/crates/test/src/rt/mod.rs +++ b/crates/test/src/rt/mod.rs @@ -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, +} + +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, - - /// The current test that is executing. If `None` no test is executing, if - /// `Some` it's the name of the tests. - current_test: RefCell>, + filter: RefCell>, /// Counter of the number of tests that have succeeded. succeeded: Cell, @@ -121,38 +133,59 @@ pub struct Context { /// Counter of the number of tests that have been ignored ignored: Cell, - /// 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>, + /// 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>, - /// 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, - current_error: RefCell, + /// Remaining tests to execute, when empty we're just waiting on the + /// `Running` tests to finish. + remaining: RefCell>, - /// Flag set as a test executes if it was actually ignored. - ignore_this_test: Cell, + /// List of currently executing tests. These tests all involve some level + /// of asynchronous work, so they're sitting on the running list. + running: RefCell>, /// How to actually format output, either node.js or browser-specific /// implementation. formatter: Box, } +/// 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>, + output: Rc>, +} + +/// 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) -> 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) -> 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); + +/// 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(&self, name: &str, f: impl FnOnce() -> F + 'static) + where F: Future + '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 + '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); + +enum Never {} + +impl Future for ExecuteTests { + type Item = bool; + type Error = Never; + + fn poll(&mut self) -> Poll { + 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) { - 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 { + output: Rc>, + test: F, +} + +#[wasm_bindgen] +extern { + #[wasm_bindgen(catch)] + fn __wbg_test_invoke(f: &mut FnMut()) -> Result<(), JsValue>; +} + +impl> Future for TestFuture { + type Item = F::Item; + type Error = F::Error; + + fn poll(&mut self) -> Poll { + 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() } } diff --git a/crates/test/src/rt/node.rs b/crates/test/src/rt/node.rs index 4e0c4c81..94da1501 100644 --- a/crates/test/src/rt/node.rs +++ b/crates/test/src/rt/node.rs @@ -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() } } diff --git a/crates/web-sys/Cargo.toml b/crates/web-sys/Cargo.toml index 7c30e17a..afca278d 100644 --- a/crates/web-sys/Cargo.toml +++ b/crates/web-sys/Cargo.toml @@ -15,11 +15,10 @@ wasm-bindgen-webidl = { path = "../webidl", version = "=0.2.15" } sourcefile = "0.1" [dependencies] -wasm-bindgen = { path = "../..", version = "=0.2.15" } - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -wasm-bindgen-test-project-builder = { path = '../test-project-builder', version = '=0.2.15' } +wasm-bindgen = { path = "../..", version = "0.2.15" } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = { path = '../test', version = '=0.2.15' } +futures = "0.1" js-sys = { path = '../js-sys', version = '0.2.0' } +wasm-bindgen-test = { path = '../test', version = '0.2.15' } +wasm-bindgen-futures = { path = '../futures', version = '0.2.15' } diff --git a/crates/web-sys/tests/all/event.rs b/crates/web-sys/tests/all/event.rs deleted file mode 100644 index c559e5a5..00000000 --- a/crates/web-sys/tests/all/event.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::websys_project; - -#[test] -fn event() { - websys_project() - .file( - "src/lib.rs", - r#" - #![feature(use_extern_macros)] - extern crate wasm_bindgen; - use wasm_bindgen::prelude::*; - extern crate web_sys; - - #[wasm_bindgen] - pub fn test_event(event: &web_sys::Event) { - // These should match `new Event`. - assert!(event.bubbles()); - assert!(event.cancelable()); - assert!(event.composed()); - - // The default behavior not initially prevented, but after - // we call `prevent_default` it better be. - assert!(!event.default_prevented()); - event.prevent_default(); - assert!(event.default_prevented()); - } - "#, - ) - .file( - "test.js", - r#" - import * as assert from "assert"; - import * as wasm from "./out"; - - export async function test() { - await new Promise(resolve => { - window.addEventListener("test-event", e => { - wasm.test_event(e); - resolve(); - }); - - window.dispatchEvent(new Event("test-event", { - bubbles: true, - cancelable: true, - composed: true, - })); - }); - } - "#, - ) - .test(); -} diff --git a/crates/web-sys/tests/all/main.rs b/crates/web-sys/tests/all/main.rs deleted file mode 100644 index 42a77768..00000000 --- a/crates/web-sys/tests/all/main.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![cfg(not(target_arch = "wasm32"))] - -extern crate wasm_bindgen_test_project_builder as project_builder; -use project_builder::{project, Project}; - -mod event; - -fn websys_project() -> Project { - project() - .add_local_dependency("web-sys", env!("CARGO_MANIFEST_DIR")) - .headless(true) - .clone() -} diff --git a/crates/web-sys/tests/wasm/event.js b/crates/web-sys/tests/wasm/event.js new file mode 100644 index 00000000..48203ecf --- /dev/null +++ b/crates/web-sys/tests/wasm/event.js @@ -0,0 +1,10 @@ +export function new_event() { + return new Promise(resolve => { + window.addEventListener("test-event", resolve); + window.dispatchEvent(new Event("test-event", { + bubbles: true, + cancelable: true, + composed: true, + })); + }); +} diff --git a/crates/web-sys/tests/wasm/event.rs b/crates/web-sys/tests/wasm/event.rs new file mode 100644 index 00000000..ca6feb66 --- /dev/null +++ b/crates/web-sys/tests/wasm/event.rs @@ -0,0 +1,29 @@ +use futures::future::Future; +use js_sys::Promise; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use wasm_bindgen_test::*; +use web_sys::Event; + +#[wasm_bindgen(module = "./tests/wasm/event.js")] +extern { + fn new_event() -> Promise; +} + +#[wasm_bindgen_test(async)] +fn event() -> impl Future { + JsFuture::from(new_event()) + .map(Event::from) + .map(|event| { + // These should match `new Event`. + assert!(event.bubbles()); + assert!(event.cancelable()); + assert!(event.composed()); + + // The default behavior not initially prevented, but after + // we call `prevent_default` it better be. + assert!(!event.default_prevented()); + event.prevent_default(); + assert!(event.default_prevented()); + }) +} diff --git a/crates/web-sys/tests/wasm/main.rs b/crates/web-sys/tests/wasm/main.rs index 5d2830c6..5306797f 100644 --- a/crates/web-sys/tests/wasm/main.rs +++ b/crates/web-sys/tests/wasm/main.rs @@ -1,8 +1,10 @@ #![feature(use_extern_macros)] #![cfg(target_arch = "wasm32")] +extern crate futures; extern crate js_sys; extern crate wasm_bindgen; +extern crate wasm_bindgen_futures; extern crate wasm_bindgen_test; extern crate web_sys; @@ -14,6 +16,7 @@ pub mod br_element; pub mod button_element; pub mod div_element; pub mod element; +pub mod event; pub mod head_element; pub mod heading_element; pub mod headers; diff --git a/src/closure.rs b/src/closure.rs index 259363a3..b2afd680 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -4,14 +4,18 @@ //! closures" from Rust to JS. Some more details can be found on the `Closure` //! type itself. +#![allow(const_err)] // FIXME(rust-lang/rust#52603) + use std::cell::UnsafeCell; use std::marker::Unsize; use std::mem::{self, ManuallyDrop}; use std::prelude::v1::*; +use std::rc::Rc; use JsValue; use convert::*; use describe::*; +use throw; /// A handle to both a closure in Rust as well as JS closure which will invoke /// the Rust closure. @@ -65,7 +69,9 @@ use describe::*; /// } /// ``` pub struct Closure { - inner: UnsafeCell>, + // Actually a `Rc` pointer, but in raw form so we can easily make copies. + // See below documentation for why this is in an `Rc`. + inner: *const UnsafeCell>, js: UnsafeCell>, } @@ -96,7 +102,7 @@ impl Closure /// This is the function where the JS closure is manufactured. pub fn wrap(t: Box) -> Closure { Closure { - inner: UnsafeCell::new(t), + inner: Rc::into_raw(Rc::new(UnsafeCell::new(t))), js: UnsafeCell::new(ManuallyDrop::new(JsValue { idx: !0 })), } } @@ -140,8 +146,8 @@ impl<'a, T> IntoWasmAbi for &'a Closure fn into_abi(self, extra: &mut Stack) -> u32 { unsafe { - let fnptr = WasmClosure::into_abi(&mut **self.inner.get(), extra); - extra.push(fnptr); + extra.push(T::invoke_fn()); + extra.push(self.inner as u32); &mut (*self.js.get()).idx as *const u32 as u32 } } @@ -166,6 +172,7 @@ impl Drop for Closure if idx != !0 { super::__wbindgen_cb_drop(idx); } + drop(Rc::from_raw(self.inner)); } } } @@ -178,9 +185,23 @@ impl Drop for Closure pub unsafe trait WasmClosure: 'static { fn describe(); - unsafe fn into_abi(me: *mut Self, extra: &mut Stack) -> u32; + fn invoke_fn() -> u32; } +// The memory safety here in these implementations below is a bit tricky. We +// want to be able to drop the `Closure` object from within the invocation of a +// `Closure` for cases like promises. That means that while it's running we +// might drop the `Closure`, but that shouldn't invalidate the environment yet. +// +// Instead what we do is to wrap closures in `Rc` variables. The main `Closure` +// has a strong reference count which keeps the trait object alive. Each +// invocation of a closure then *also* clones this and gets a new reference +// count. When the closure returns it will release the reference count. +// +// This means that if the main `Closure` is dropped while it's being invoked +// then destruction is deferred until execution returns. Otherwise it'll +// deallocate data immediately. + macro_rules! doit { ($( ($($var:ident)*) @@ -193,11 +214,30 @@ macro_rules! doit { <&Self>::describe(); } - unsafe fn into_abi(me: *mut Self, extra: &mut Stack) -> u32 { - IntoWasmAbi::into_abi(&*me, extra) + fn invoke_fn() -> u32 { + #[allow(non_snake_case)] + unsafe extern fn invoke<$($var: FromWasmAbi,)*>( + a: *const UnsafeCell>, + $($var: <$var as FromWasmAbi>::Abi),* + ) { + if a.is_null() { + throw("closure invoked recursively or destroyed already"); + } + let a = Rc::from_raw(a); + let my_handle = a.clone(); + drop(Rc::into_raw(a)); + let f: &Fn($($var),*) = &**my_handle.get(); + let mut _stack = GlobalStack::new(); + $( + let $var = <$var as FromWasmAbi>::from_abi($var, &mut _stack); + )* + f($($var),*) + } + invoke::<$($var,)*> as u32 } } - // Fn with return + + // Fn with no return unsafe impl<$($var,)* R> WasmClosure for Fn($($var),*) -> R where $($var: FromWasmAbi + 'static,)* R: IntoWasmAbi + 'static, @@ -206,8 +246,26 @@ macro_rules! doit { <&Self>::describe(); } - unsafe fn into_abi(me: *mut Self, extra: &mut Stack) -> u32 { - IntoWasmAbi::into_abi(&*me, extra) + fn invoke_fn() -> u32 { + #[allow(non_snake_case)] + unsafe extern fn invoke<$($var: FromWasmAbi,)* R: IntoWasmAbi>( + a: *const UnsafeCell R>>, + $($var: <$var as FromWasmAbi>::Abi),* + ) -> ::Abi { + if a.is_null() { + throw("closure invoked recursively or destroyed already"); + } + let a = Rc::from_raw(a); + let my_handle = a.clone(); + drop(Rc::into_raw(a)); + let f: &Fn($($var),*) -> R = &**my_handle.get(); + let mut _stack = GlobalStack::new(); + $( + let $var = <$var as FromWasmAbi>::from_abi($var, &mut _stack); + )* + f($($var),*).into_abi(&mut GlobalStack::new()) + } + invoke::<$($var,)* R> as u32 } } // FnMut with no return @@ -218,11 +276,30 @@ macro_rules! doit { <&mut Self>::describe(); } - unsafe fn into_abi(me: *mut Self, extra: &mut Stack) -> u32 { - IntoWasmAbi::into_abi(&mut *me, extra) + fn invoke_fn() -> u32 { + #[allow(non_snake_case)] + unsafe extern fn invoke<$($var: FromWasmAbi,)*>( + a: *const UnsafeCell>, + $($var: <$var as FromWasmAbi>::Abi),* + ) { + if a.is_null() { + throw("closure invoked recursively or destroyed already"); + } + let a = Rc::from_raw(a); + let my_handle = a.clone(); + drop(Rc::into_raw(a)); + let f: &mut FnMut($($var),*) = &mut **my_handle.get(); + let mut _stack = GlobalStack::new(); + $( + let $var = <$var as FromWasmAbi>::from_abi($var, &mut _stack); + )* + f($($var),*) + } + invoke::<$($var,)*> as u32 } } - // FnMut with return + + // Fn with no return unsafe impl<$($var,)* R> WasmClosure for FnMut($($var),*) -> R where $($var: FromWasmAbi + 'static,)* R: IntoWasmAbi + 'static, @@ -231,8 +308,26 @@ macro_rules! doit { <&mut Self>::describe(); } - unsafe fn into_abi(me: *mut Self, extra: &mut Stack) -> u32 { - IntoWasmAbi::into_abi(&mut *me, extra) + fn invoke_fn() -> u32 { + #[allow(non_snake_case)] + unsafe extern fn invoke<$($var: FromWasmAbi,)* R: IntoWasmAbi>( + a: *const UnsafeCell R>>, + $($var: <$var as FromWasmAbi>::Abi),* + ) -> ::Abi { + if a.is_null() { + throw("closure invoked recursively or destroyed already"); + } + let a = Rc::from_raw(a); + let my_handle = a.clone(); + drop(Rc::into_raw(a)); + let f: &mut FnMut($($var),*) -> R = &mut **my_handle.get(); + let mut _stack = GlobalStack::new(); + $( + let $var = <$var as FromWasmAbi>::from_abi($var, &mut _stack); + )* + f($($var),*).into_abi(&mut GlobalStack::new()) + } + invoke::<$($var,)* R> as u32 } } )*)